]> granicus.if.org Git - python/commitdiff
Ported some features from zope:
authorJim Fulton <jim@zope.com>
Wed, 14 Jul 2004 19:06:50 +0000 (19:06 +0000)
committerJim Fulton <jim@zope.com>
Wed, 14 Jul 2004 19:06:50 +0000 (19:06 +0000)
- Fixed the display of tests in verbose output
- Allow setUp and tearDown functions to be provided for DocTestSuites.

Lib/doctest.py

index 611897dc89d64a4b4adf46e6af661bfa1368c4ad..69047db516de6f3588636913ff1360f8db47f115 100644 (file)
@@ -289,7 +289,6 @@ __all__ = [
     'run_docstring_examples',
     'is_private',
     'Tester',
-    'DocTestTestFailure',
     'DocTestSuite',
     'testsource',
     'debug',
@@ -1289,66 +1288,92 @@ def _find_tests(module, prefix=None):
     _extract_doctests(mdict.items(), module, mdict, tests, prefix)
     return tests
 
-# unittest helpers.
+###############################################################################
+# unitest support
 
-# A function passed to unittest, for unittest to drive.
-# tester is doctest Tester instance.  doc is the docstring whose
-# doctests are to be run.
+from StringIO import StringIO
+import os
+import sys
+import tempfile
+import unittest
 
-def _utest(tester, name, doc, filename, lineno):
-    import sys
-    from StringIO import StringIO
+class DocTestTestCase(unittest.TestCase):
+    """A test case that wraps a test function.
 
-    old = sys.stdout
-    sys.stdout = new = StringIO()
-    try:
-        failures, tries = tester.runstring(doc, name)
-    finally:
-        sys.stdout = old
-
-    if failures:
-        msg = new.getvalue()
-        lname = '.'.join(name.split('.')[-1:])
-        if not lineno:
-            lineno = "0 (don't know line number)"
-        # Don't change this format!  It was designed so that Emacs can
-        # parse it naturally.
-        raise DocTestTestFailure('Failed doctest test for %s\n'
-                                 '  File "%s", line %s, in %s\n\n%s' %
-                                 (name, filename, lineno, lname, msg))
-
-class DocTestTestFailure(Exception):
-    """A doctest test failed"""
-
-def DocTestSuite(module=None):
-    """Convert doctest tests for a module to a unittest TestSuite.
-
-    The returned TestSuite is to be run by the unittest framework, and
-    runs each doctest in the module.  If any of the doctests fail,
-    then the synthesized unit test fails, and an error is raised showing
-    the name of the file containing the test and a (sometimes approximate)
-    line number.
-
-    The optional module argument provides the module to be tested.  It
-    can be a module object or a (possibly dotted) module name.  If not
-    specified, the module calling DocTestSuite() is used.
-
-    Example (although note that unittest supplies many ways to use the
-    TestSuite returned; see the unittest docs):
-
-        import unittest
-        import doctest
-        import my_module_with_doctests
-
-        suite = doctest.DocTestSuite(my_module_with_doctests)
-        runner = unittest.TextTestRunner()
-        runner.run(suite)
+    This is useful for slipping pre-existing test functions into the
+    PyUnit framework. Optionally, set-up and tidy-up functions can be
+    supplied. As with TestCase, the tidy-up ('tearDown') function will
+    always be called if the set-up ('setUp') function ran successfully.
     """
 
-    import unittest
+    def __init__(self, tester, name, doc, filename, lineno,
+                 setUp=None, tearDown=None):
+        unittest.TestCase.__init__(self)
+        (self.__tester, self.__name, self.__doc,
+         self.__filename, self.__lineno,
+         self.__setUp, self.__tearDown
+         ) = tester, name, doc, filename, lineno, setUp, tearDown
+
+    def setUp(self):
+        if self.__setUp is not None:
+            self.__setUp()
+
+    def tearDown(self):
+        if self.__tearDown is not None:
+            self.__tearDown()
+
+    def runTest(self):
+        old = sys.stdout
+        new = StringIO()
+        try:
+            sys.stdout = new
+            failures, tries = self.__tester.runstring(self.__doc, self.__name)
+        finally:
+            sys.stdout = old
+
+        if failures:
+            lname = '.'.join(self.__name.split('.')[-1:])
+            lineno = self.__lineno or "0 (don't know line no)"
+            raise self.failureException(
+                'Failed doctest test for %s\n'
+                '  File "%s", line %s, in %s\n\n%s'
+                % (self.__name, self.__filename, lineno, lname, new.getvalue())
+                )
+
+    def id(self):
+        return self.__name
+
+    def __repr__(self):
+        name = self.__name.split('.')
+        return "%s (%s)" % (name[-1], '.'.join(name[:-1]))
+
+    __str__ = __repr__
+
+    def shortDescription(self):
+        return "Doctest: " + self.__name
+
+
+def DocTestSuite(module=None,
+                 setUp=lambda: None,
+                 tearDown=lambda: None,
+                 ):
+    """Convert doctest tests for a mudule to a unittest test suite
+
+    This tests convers each documentation string in a module that
+    contains doctest tests to a unittest test case. If any of the
+    tests in a doc string fail, then the test case fails. An error is
+    raised showing the name of the file containing the test and a
+    (sometimes approximate) line number.
+
+    A module argument provides the module to be tested. The argument
+    can be either a module or a module name.
+
+    If no argument is given, the calling module is used.
+
+    """
+    module = _normalizeModule(module)
+    tests = _findTests(module)
 
-    module = _normalize_module(module)
-    tests = _find_tests(module)
     if not tests:
         raise ValueError(module, "has no tests")
 
@@ -1362,78 +1387,166 @@ def DocTestSuite(module=None):
                 filename = filename[:-1]
             elif filename.endswith(".pyo"):
                 filename = filename[:-1]
-        def runit(name=name, doc=doc, filename=filename, lineno=lineno):
-            _utest(tester, name, doc, filename, lineno)
-        suite.addTest(unittest.FunctionTestCase(
-                                    runit,
-                                    description="doctest of " + name))
+
+        suite.addTest(DocTestTestCase(
+            tester, name, doc, filename, lineno,
+            setUp, tearDown))
+
     return suite
 
-# Debugging support.
+def _normalizeModule(module):
+    # Normalize a module
+    if module is None:
+        # Test the calling module
+        module = sys._getframe(2).f_globals['__name__']
+        module = sys.modules[module]
+
+    elif isinstance(module, (str, unicode)):
+        module = __import__(module, globals(), locals(), ["*"])
+
+    return module
+
+def _doc(name, object, tests, prefix, filename='', lineno=''):
+    doc = getattr(object, '__doc__', '')
+    if doc and doc.find('>>>') >= 0:
+        tests.append((prefix+name, doc, filename, lineno))
+
+
+def _findTests(module, prefix=None):
+    if prefix is None:
+        prefix = module.__name__
+    dict = module.__dict__
+    tests = []
+    _doc(prefix, module, tests, '',
+         lineno="1 (or below)")
+    prefix = prefix and (prefix + ".")
+    _find(dict.items(), module, dict, tests, prefix)
+    return tests
+
+def _find(items, module, dict, tests, prefix, minlineno=0):
+    for name, object in items:
+
+        # Only interested in named objects
+        if not hasattr(object, '__name__'):
+            continue
+
+        if hasattr(object, 'func_globals'):
+            # Looks like a func
+            if object.func_globals is not dict:
+                # Non-local func
+                continue
+            code = getattr(object, 'func_code', None)
+            filename = getattr(code, 'co_filename', '')
+            lineno = getattr(code, 'co_firstlineno', -1) + 1
+            if minlineno:
+                minlineno = min(lineno, minlineno)
+            else:
+                minlineno = lineno
+            _doc(name, object, tests, prefix, filename, lineno)
+
+        elif hasattr(object, "__module__"):
+            # Maybe a class-like things. In which case, we care
+            if object.__module__ != module.__name__:
+                continue # not the same module
+            if not (hasattr(object, '__dict__')
+                    and hasattr(object, '__bases__')):
+                continue # not a class
+
+            lineno = _find(object.__dict__.items(), module, dict, tests,
+                           prefix+name+".")
+
+            _doc(name, object, tests, prefix,
+                 lineno="%s (or above)" % (lineno-3))
+
+    return minlineno
+
+# end unitest support
+###############################################################################
+
+###############################################################################
+# debugger
 
 def _expect(expect):
-    # Return the expected output (if any), formatted as a Python
-    # comment block.
+    # Return the expected output, if any
     if expect:
         expect = "\n# ".join(expect.split("\n"))
         expect = "\n# Expect:\n# %s" % expect
     return expect
 
 def testsource(module, name):
-    """Extract the doctest examples from a docstring.
+    """Extract the test sources from a doctest test docstring as a script
 
     Provide the module (or dotted name of the module) containing the
-    tests to be extracted, and the name (within the module) of the object
-    with the docstring containing the tests to be extracted.
+    test to be debugged and the name (within the module) of the object
+    with the doc string with tests to be debugged.
 
-    The doctest examples are returned as a string containing Python
-    code.  The expected output blocks in the examples are converted
-    to Python comments.
     """
-
-    module = _normalize_module(module)
-    tests = _find_tests(module, "")
-    test = [doc for (tname, doc, dummy, dummy) in tests
-                if tname == name]
+    module = _normalizeModule(module)
+    tests = _findTests(module, "")
+    test = [doc for (tname, doc, f, l) in tests if tname == name]
     if not test:
         raise ValueError(name, "not found in tests")
     test = test[0]
-    examples = [source + _expect(expect)
-                for source, expect, dummy in _extract_examples(test)]
-    return '\n'.join(examples)
-
-def debug(module, name):
-    """Debug a single docstring containing doctests.
-
-    Provide the module (or dotted name of the module) containing the
-    docstring to be debugged, and the name (within the module) of the
-    object with the docstring to be debugged.
-
-    The doctest examples are extracted (see function testsource()),
-    and written to a temp file.  The Python debugger (pdb) is then
-    invoked on that file.
+    # XXX we rely on an internal doctest function:
+    examples = _extract_examples(test)
+    testsrc = '\n'.join([
+        "%s%s" % (source, _expect(expect))
+        for (source, expect, lineno) in examples
+        ])
+    return testsrc
+
+def debug_src(src, pm=False, globs=None):
+    """Debug a single doctest test doc string
+
+    The string is provided directly
     """
-
-    import os
+    # XXX we rely on an internal doctest function:
+    examples = _extract_examples(src)
+    src = '\n'.join([
+        "%s%s" % (source, _expect(expect))
+        for (source, expect, lineno) in examples
+        ])
+    debug_script(src, pm, globs)
+
+def debug_script(src, pm=False, globs=None):
+    "Debug a test script"
     import pdb
-    import tempfile
 
-    module = _normalize_module(module)
-    testsrc = testsource(module, name)
     srcfilename = tempfile.mktemp("doctestdebug.py")
-    f = file(srcfilename, 'w')
-    f.write(testsrc)
-    f.close()
+    open(srcfilename, 'w').write(src)
+    if globs:
+        globs = globs.copy()
+    else:
+        globs = {}
 
-    globs = {}
-    globs.update(module.__dict__)
     try:
-        # Note that %r is vital here.  '%s' instead can, e.g., cause
-        # backslashes to get treated as metacharacters on Windows.
-        pdb.run("execfile(%r)" % srcfilename, globs, globs)
+        if pm:
+            try:
+                execfile(srcfilename, globs, globs)
+            except:
+                print sys.exc_info()[1]
+                pdb.post_mortem(sys.exc_info()[2])
+        else:
+            # Note that %r is vital here.  '%s' instead can, e.g., cause
+            # backslashes to get treated as metacharacters on Windows.
+            pdb.run("execfile(%r)" % srcfilename, globs, globs)
     finally:
         os.remove(srcfilename)
 
+def debug(module, name, pm=False):
+    """Debug a single doctest test doc string
+
+    Provide the module (or dotted name of the module) containing the
+    test to be debugged and the name (within the module) of the object
+    with the doc string with tests to be debugged.
+
+    """
+    module = _normalizeModule(module)
+    testsrc = testsource(module, name)
+    debug_script(testsrc, pm, module.__dict__)
+
+# end debugger
+###############################################################################
 
 
 class _TestClass: