summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--numpy/testing/noseclasses.py249
-rw-r--r--numpy/testing/nosetester.py261
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():