[Zope3-checkins] SVN: Zope3/trunk/src/zope/testing/doctestunit.py Added a new invert_src function (may end up renaming this later)

Jim Fulton jim at zope.com
Wed Jul 21 18:51:08 EDT 2004


Log message for revision 26666:
  
  Added a new invert_src function (may end up renaming this later)
  that takes a doctest and changes it to a script.  This turns examples
  into code and everything else into comments.  Changed the debugger to
  use this, making debugging far more pleasent.
  
  Changed DocFileSuite:
  
  - If a package is passed, it must be passed as a keyword argument.
    (For now, a package can also be passed before passing any file
    names, but this generates a deprecation warning.)
  
  - File paths may contain '/'s as separators. These will be converted
    to native file separators at run time.
  
  - It is now possible to pass set-up and tear-down methods
  
  - It is now possible to pass a dictionary of initial global variables.
  
  - Error output is improved as is meta data used for verbose output.
  


Changed:
  U   Zope3/trunk/src/zope/testing/doctestunit.py


-=-
Modified: Zope3/trunk/src/zope/testing/doctestunit.py
===================================================================
--- Zope3/trunk/src/zope/testing/doctestunit.py	2004-07-21 22:49:38 UTC (rev 26665)
+++ Zope3/trunk/src/zope/testing/doctestunit.py	2004-07-21 22:51:08 UTC (rev 26666)
@@ -40,21 +40,21 @@
     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
+        (self._dt_tester, self._dt_name, self._dt_doc,
+         self._dt_filename, self._dt_lineno,
+         self._dt_setUp, self._dt_tearDown
          ) = tester, name, doc, filename, lineno, setUp, tearDown
 
     def setUp(self):
-        if self.__setUp is not None:
-            self.__setUp()
+        if self._dt_setUp is not None:
+            self._dt_setUp()
 
     def tearDown(self):
-        if self.__tearDown is not None:
-            self.__tearDown()
+        if self._dt_tearDown is not None:
+            self._dt_tearDown()
 
     def setDebugModeOn(self):
-        self.__tester.optionflags |= (
+        self._dt_tester.optionflags |= (
             doctest.RUN_DEBUGGER_ON_UNEXPECTED_EXCEPTION)
 
     def runTest(self):
@@ -62,68 +62,98 @@
         new = StringIO()
         try:
             sys.stdout = new
-            failures, tries = self.__tester.runstring(self.__doc, self.__name)
+            failures, tries = self._dt_tester.runstring(
+                self._dt_doc, self._dt_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'
+            raise self.failureException(self.format_failure(new.getvalue()))
+
+    def format_failure(self, err):
+        lineno = self._dt_lineno or "0 (don't know line no)"
+        lname = '.'.join(self._dt_name.split('.')[-1:]) 
+        return ('Failed doctest test for %s\n'
                 '  File "%s", line %s, in %s\n\n%s'
-                % (self.__name, self.__filename, lineno, lname, new.getvalue())
+                % (self._dt_name, self._dt_filename,
+                   lineno, lname, err)
                 )
 
     def id(self):
-        return self.__name
+        return self._dt_name
 
     def __repr__(self):
-        name = self.__name.split('.')
+        name = self._dt_name.split('.')
         return "%s (%s)" % (name[-1], '.'.join(name[:-1]))
 
     __str__ = __repr__
 
     def shortDescription(self):
-        return "Doctest: " + self.__name
+        return "Doctest: " + self._dt_name
 
-def DocFileSuite(package, *paths):
+class DocTestFileTestCase(DocTestTestCase):
+
+    def __repr__(self):
+        return self._dt_name
+    __str__ = __repr__
+
+    def format_failure(self, err):
+        return ('Failed doctest test for %s\n  File "%s", line 0\n\n%s'
+                % (self._dt_name, self._dt_filename, err)
+                )
+
+def DocFileTest(path, package=None, globs=None,
+                setUp=None, tearDown=None,
+                ):
+    
+    package = _normalizeModule(package)
+    name = path.split('/')[-1]
+    dir = os.path.split(package.__file__)[0]
+    path = os.path.join(dir, *(path.split('/')))
+    doc = open(path).read()
+    tester = doctest.Tester(globs=(globs or {}))
+    return DocTestFileTestCase(tester, name, doc, path, 0, setUp, tearDown)
+
+def DocFileSuite(*paths, **kw):
     """Creates a suite of doctest files.
-    
-    package is the source package containing the doctest files.
-    
-    Each subsequent argument is a string specifying the file name of the
-    doctest relative to the package.
-    
-    The suite contains one function test case (unittest.FunctionTestCase) for 
-    each doctest source file. The function name is derrived from the doctest
-    file name by replacing '.' characters with '_'. E.g. if the doctest file is
-    'utility.txt' and the package is foo.bar, the test function name will be 
-    'foo.bar.utility_txt'.
+
+    One or more text file paths are given as strings.  These should
+    use "/" characters to separate path segments.  Paths are relative
+    to the directory of the calling module, or relative to the package
+    passed as a keyword argument.
+
+    A number of options may be provided as keyword arguments:
+
+    package
+      The name of a Python package. Text-file paths will be
+      interpreted relative to the directory containing this package.
+      The package may be supplied as a package object or as a dotted
+      package name.
+
+    setUp
+      The name of a set-up function. This is called before running the
+      tests in each file.
+
+    tearDown
+      The name of a tear-down function. This is called after running the
+      tests in each file.
+
+    globs
+      A dictionary containing initial global variables for the tests.
     """
+    # BBB temporarily support passing package as first argument
+    if not isinstance(paths[0], basestring):
+        import warnings
+        warnings.warn("DocFileSuite package arguemnt must be provided as a "
+                      "keyword argument",
+                      DeprecationWarning, 2)
+        kw = kw.copy()
+        kw['package'] = paths[0]
+        paths = paths[1:]
     
-    # It's not entirely obvious how to connection this single string
-    # with unittest.  For now, re-use the _utest() function that comes
-    # standard with doctest in Python 2.3.  One problem is that the
-    # error indicator doesn't point to the line of the doctest file
-    # that failed.
-    import os, doctest, new
-    t = doctest.Tester(globs={})
     suite = unittest.TestSuite()
-    dir = os.path.split(package.__file__)[0]
     for path in paths:
-        from re import sub
-        funcName = path.replace('.', '_')
-        funcName = package.__name__ + '.' + sub('\\\|/', '.', funcName)
-        path = os.path.join(dir, path)
-        source = open(path).read()
-        def runit(path=path, source=source):
-            doctest._utest(t, path, source, path, 0)
-        runit = new.function(runit.func_code, runit.func_globals, funcName,
-                             runit.func_defaults, runit.func_closure)
-        f = unittest.FunctionTestCase(runit,
-                                      description="doctest from %s" % path)
-        suite.addTest(f)
+        suite.addTest(DocFileTest(path, **kw))
     return suite
 
 def DocTestSuite(module=None,
@@ -240,13 +270,63 @@
 ####################################################################
 # doctest debugger
 
-def _expect(expect):
-    # Return the expected output, if any
-    if expect:
-        expect = "\n# ".join(expect.split("\n"))
-        expect = "\n# Expect:\n# %s" % expect
-    return expect
+def invert_src(s):
+    """Invert a doctest
 
+    Examples become regular code. Everything else becomes comments    
+    """
+    isPS1, isPS2 = doctest._isPS1, doctest._isPS2
+    isEmpty, isComment = doctest._isEmpty, doctest._isComment
+    output = []
+    lines = s.split("\n")
+    i, n = 0, len(lines)
+    while i < n:
+        line = lines[i]
+        i = i + 1
+        m = isPS1(line)
+        if m is None:
+            output.append('#  '+line)
+            continue
+        j = m.end(0)  # beyond the prompt
+        if isEmpty(line, j) or isComment(line, j):
+            # a bare prompt or comment -- not interesting
+            output.append('#  '+line[j:])
+
+        lineno = i - 1
+        if line[j] != " ":
+            raise ValueError("line %r of docstring lacks blank after %s: %s" %
+                             (lineno, PS1, line))
+        j = j + 1
+        blanks = m.group(1)
+        nblanks = len(blanks)
+        # suck up this and following PS2 lines
+        while 1:
+            output.append(line[j:])
+            line = lines[i]
+            m = isPS2(line)
+            if m:
+                if m.group(1) != blanks:
+                    raise ValueError("inconsistent leading whitespace "
+                        "in line %r of docstring: %s" % (i, line))
+                i = i + 1
+            else:
+                break
+
+        # suck up response
+        if not (isPS1(line) or isEmpty(line)):
+            while 1:
+                if line[:nblanks] != blanks:
+                    raise ValueError("inconsistent leading whitespace "
+                        "in line %r of docstring: %s" % (i, line))
+                output.append('#'+line[nblanks:])
+                i = i + 1
+                line = lines[i]
+                if isPS1(line) or isEmpty(line):
+                    break
+
+    return '\n'.join(output)
+
+
 def testsource(module, name):
     """Extract the test sources from a doctest test docstring as a script
 
@@ -260,26 +340,15 @@
     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]
-    # TODO: we rely on an internal doctest function:
-    examples = doctest._extract_examples(test)
-    testsrc = '\n'.join([
-        "%s%s" % (source, _expect(expect))
-        for (source, expect, lineno) in examples
-        ])
-    return testsrc
+    return invert_src(test[0])
 
 def debug_src(src, pm=False, globs=None):
     """Debug a single doctest test doc string
 
     The string is provided directly
     """
-    # TODO: we rely on an internal doctest function:
-    examples = doctest._extract_examples(src)
-    src = '\n'.join([
-        "%s%s" % (source, _expect(expect))
-        for (source, expect, lineno) in examples
-        ])
+
+    src = invert_src(src)
     debug_script(src, pm, globs)
 
 def debug_script(src, pm=False, globs=None):



More information about the Zope3-Checkins mailing list