[Numpy-svn] r5450 - trunk/numpy/testing

numpy-svn@scip... numpy-svn@scip...
Fri Jul 18 07:32:30 CDT 2008


Author: alan.mcintyre
Date: 2008-07-18 07:32:27 -0500 (Fri, 18 Jul 2008)
New Revision: 5450

Added:
   trunk/numpy/testing/noseclasses.py
Modified:
   trunk/numpy/testing/nosetester.py
Log:
Use a subclass of the nose doctest plugin instead of monkeypatching the builtin plugin.
Removed decorators for NoseTester methods.


Added: trunk/numpy/testing/noseclasses.py
===================================================================
--- trunk/numpy/testing/noseclasses.py	2008-07-18 02:02:19 UTC (rev 5449)
+++ trunk/numpy/testing/noseclasses.py	2008-07-18 12:32:27 UTC (rev 5450)
@@ -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)

Modified: trunk/numpy/testing/nosetester.py
===================================================================
--- trunk/numpy/testing/nosetester.py	2008-07-18 02:02:19 UTC (rev 5449)
+++ trunk/numpy/testing/nosetester.py	2008-07-18 12:32:27 UTC (rev 5450)
@@ -7,127 +7,28 @@
 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 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)
 
-    def __init__(self):
-        if NoseCustomizer.__patched:
-            return
+    # if package name determination failed, just default to numpy/scipy
+    if not pkg_name:
+        if 'scipy' in fullpath:
+            return 'scipy'
+        else:
+            return 'numpy'
 
-        NoseCustomizer.__patched = True
+    # otherwise, reverse to get correct order and return
+    pkg_name.reverse()
+    return '.'.join(pkg_name)
 
-        # 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
-
-        # 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
-
-
 def import_nose():
     """ Import nose only when needed.
     """
@@ -143,12 +44,11 @@
             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,7 +60,31 @@
 
     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 @@
 
         # 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)
+        self.package_name = get_package_name(package)
 
-        # 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
-
-        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 @@
         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 @@
             (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 @@
 
         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 @@
                 """
                 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 @@
                 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 @@
         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():



More information about the Numpy-svn mailing list