123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- # These classes implement a doctest runner plugin for nose, a "known failure"
- # error class, and a customized TestProgram for NumPy.
- # 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 sys
- import doctest
- import inspect
- import numpy
- import nose
- from nose.plugins import doctests as npd
- from nose.plugins.errorclass import ErrorClass, ErrorClassPlugin
- from nose.plugins.base import Plugin
- from nose.util import src
- from .nosetester import get_package_name
- from .utils import KnownFailureException, KnownFailureTest
- # Some of 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, https://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:
- return True
- elif inspect.isfunction(object):
- return module.__dict__ is object.__globals__
- elif inspect.isbuiltin(object):
- return module.__name__ == object.__module__
- elif inspect.isclass(object):
- 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
- return module.__name__ == object.__self__.__class__.__module__
- elif inspect.getmodule(object) is not None:
- return module is inspect.getmodule(object)
- elif hasattr(object, '__module__'):
- return module.__name__ == object.__module__
- elif isinstance(object, property):
- 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, isfunction, ismethod
- )
- # Look for tests in a module's contained objects.
- if ismodule(obj) and self._recurse:
- for valname, val in obj.__dict__.items():
- valname1 = f'{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 isclass(obj) and self._recurse:
- for valname, val in obj.__dict__.items():
- # Special handling for staticmethod/classmethod.
- if isinstance(val, staticmethod):
- val = getattr(obj, valname)
- if isinstance(val, classmethod):
- val = getattr(obj, valname).__func__
- # Recurse to methods, properties, and nested classes.
- if ((isfunction(val) or isclass(val) or
- ismethod(val) or isinstance(val, property)) and
- self._from_module(module, val)):
- valname = f'{name}.{valname}'
- self._find(tests, val, valname, module, source_lines,
- globs, seen)
- # 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
- # it would be useful to normalize endianness so that
- # bigendian machines don't fail all the tests (and there are
- # actually some bigendian examples in the doctests). Let's try
- # making them all little endian
- got = got.replace("'>", "'<")
- want = want.replace("'>", "'<")
- # try to normalize out 32 and 64 bit default int sizes
- for sz in [4, 8]:
- got = got.replace("'<i%d'" % sz, "int")
- want = want.replace("'<i%d'" % sz, "int")
- ret = doctest.OutputChecker.check_output(self, want,
- got, optionflags)
- 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
- score = 1000 # load late, after doctest builtin
- # always use whitespace and ellipsis options for doctests
- doctest_optflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
- # files that should be ignored for doctests
- doctest_ignore = ['generate_numpy_api.py',
- 'setup.py']
- # Custom classes; class variables to allow subclassing
- doctest_case_class = NumpyDocTestCase
- out_check_class = NumpyOutputChecker
- test_finder_class = NumpyDocTestFinder
- # Don't use the standard doctest option handler; hard-code the option values
- def options(self, parser, env=os.environ):
- Plugin.options(self, parser, env)
- # Test doctests in 'test' files / directories. Standard plugin default
- # is False
- self.doctest_tests = True
- # Variable name; if defined, doctest results stored in this variable in
- # the top-level namespace. None is the standard default
- self.doctest_result_var = None
- def configure(self, options, config):
- # parent method sets enabled flag from command line --with-numpydoctest
- Plugin.configure(self, options, config)
- self.finder = self.test_finder_class()
- self.parser = doctest.DocTestParser()
- if self.enabled:
- # Pull standard doctest out of plugin list; there's no reason to run
- # both. In practice the Unplugger plugin above would cover us when
- # run from a standard numpy.test() call; this is just in case
- # someone wants to run our plugin outside the numpy.test() machinery
- config.plugins.plugins = [p for p in config.plugins.plugins
- if p.name != 'doctest']
- def set_test_context(self, test):
- """ Configure `test` object to set test context
- We set the numpy / scipy standard doctest namespace
- Parameters
- ----------
- test : test object
- with ``globs`` dictionary defining namespace
- Returns
- -------
- None
- Notes
- -----
- `test` object modified in place
- """
- # set the namespace for tests
- 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('.')
- p2 = p[-1]
- test.globs[p2] = __import__(pkg_name, test.globs, {}, [p2])
- # Override test loading to customize test context (with set_test_context
- # method), set standard docstring options, and install our own test output
- # checker
- 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
- # Set test namespace; test altered in place
- self.set_test_context(test)
- yield self.doctest_case_class(test,
- optionflags=self.doctest_optflags,
- checker=self.out_check_class(),
- result_var=self.doctest_result_var)
- # 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)
- # Ignore NumPy-specific build files that shouldn't be searched for tests
- def wantFile(self, file):
- bn = os.path.basename(file)
- if bn in self.doctest_ignore:
- return False
- return npd.Doctest.wantFile(self, file)
- class Unplugger:
- """ Nose plugin to remove named plugin late in loading
- By default it removes the "doctest" plugin.
- """
- name = 'unplugger'
- enabled = True # always enabled
- score = 4000 # load late in order to be after builtins
- def __init__(self, to_unplug='doctest'):
- self.to_unplug = to_unplug
- def options(self, parser, env):
- pass
- def configure(self, options, config):
- # Pull named plugin out of plugins list
- config.plugins.plugins = [p for p in config.plugins.plugins
- if p.name != self.to_unplug]
- class KnownFailurePlugin(ErrorClassPlugin):
- '''Plugin that installs a KNOWNFAIL error class for the
- KnownFailureClass exception. When KnownFailure is raised,
- the exception will be logged in the knownfail attribute of the
- result, 'K' or 'KNOWNFAIL' (verbose) will be output, and the
- exception will not be counted as an error or failure.'''
- enabled = True
- knownfail = ErrorClass(KnownFailureException,
- label='KNOWNFAIL',
- isfailure=False)
- def options(self, parser, env=os.environ):
- env_opt = 'NOSE_WITHOUT_KNOWNFAIL'
- parser.add_option('--no-knownfail', action='store_true',
- dest='noKnownFail', default=env.get(env_opt, False),
- help='Disable special handling of KnownFailure '
- 'exceptions')
- def configure(self, options, conf):
- if not self.can_configure:
- return
- self.conf = conf
- disable = getattr(options, 'noKnownFail', False)
- if disable:
- self.enabled = False
- KnownFailure = KnownFailurePlugin # backwards compat
- class FPUModeCheckPlugin(Plugin):
- """
- Plugin that checks the FPU mode before and after each test,
- raising failures if the test changed the mode.
- """
- def prepareTestCase(self, test):
- from numpy.core._multiarray_tests import get_fpu_mode
- def run(result):
- old_mode = get_fpu_mode()
- test.test(result)
- new_mode = get_fpu_mode()
- if old_mode != new_mode:
- try:
- raise AssertionError(
- "FPU mode changed from {0:#x} to {1:#x} during the "
- "test".format(old_mode, new_mode))
- except AssertionError:
- result.addFailure(test, sys.exc_info())
- return run
- # Class allows us to save the results of the tests in runTests - see runTests
- # method docstring for details
- class NumpyTestProgram(nose.core.TestProgram):
- def runTests(self):
- """Run Tests. Returns true on success, false on failure, and
- sets self.success to the same value.
- Because nose currently discards the test result object, but we need
- to return it to the user, override TestProgram.runTests to retain
- the result
- """
- if self.testRunner is None:
- self.testRunner = nose.core.TextTestRunner(stream=self.config.stream,
- 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
- self.result = self.testRunner.run(self.test)
- self.success = self.result.wasSuccessful()
- return self.success
|