Spaces:
Paused
Paused
| # 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 | |