[Zope3-checkins] SVN: Zope3/branches/ZopeX3-3.0/src/zope/ Merged from trunk:

Jim Fulton jim at zope.com
Mon Aug 23 19:55:09 EDT 2004


Log message for revision 27239:
  Merged from trunk:
  
    r27216 | jim | 2004-08-22 11:13:31 -0400 (Sun, 22 Aug 2004) | 15 lines
  
  Integrated latest doctest from Python CVS head.
  
  You can't set global test options within tests anymore.
  You can set options for individual examples, or you can
  supply option flags when you create a test suite.
  
  Failure output formatting is much improved.
  
  Can now use:
  
    import pdb; pdb.set_trace()
  
  when debugging doctests.
  
  


Changed:
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/container/tests/test_contained.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/interface/README.txt
  U   Zope3/branches/ZopeX3-3.0/src/zope/interface/tests/test_interface.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py


-=-
Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/container/tests/test_contained.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/container/tests/test_contained.py	2004-08-23 23:50:00 UTC (rev 27238)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/container/tests/test_contained.py	2004-08-23 23:55:09 UTC (rev 27239)
@@ -19,7 +19,7 @@
 from persistent import Persistent
 
 import zope.interface
-from zope.testing.doctestunit import DocTestSuite
+from zope.testing import doctest
 
 from zope.app.tests.placelesssetup import setUp, tearDown
 from zope.app.container.contained import ContainedProxy
@@ -84,10 +84,7 @@
     """
 
 def test_declarations_on_ContainedProxy():
-    """
-
-    ..Ignoe whitespace differences
-      >>> doctest: +NORMALIZE_WHITESPACE
+    r"""
     
     It is possible to make declarations on ContainedProxy objects.
 
@@ -292,9 +289,9 @@
 
 def test_suite():
     return unittest.TestSuite((
-        DocTestSuite('zope.app.container.contained',
-                     setUp=setUp, tearDown=tearDown),
-        DocTestSuite(),
+        doctest.DocTestSuite('zope.app.container.contained',
+                             setUp=setUp, tearDown=tearDown),
+        doctest.DocTestSuite(optionflags=doctest.NORMALIZE_WHITESPACE),
         ))
 
 if __name__ == '__main__': unittest.main()

Modified: Zope3/branches/ZopeX3-3.0/src/zope/interface/README.txt
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/interface/README.txt	2004-08-23 23:50:00 UTC (rev 27238)
+++ Zope3/branches/ZopeX3-3.0/src/zope/interface/README.txt	2004-08-23 23:55:09 UTC (rev 27239)
@@ -4,11 +4,6 @@
 
 .. contents::
 
-.. Doctest directive that causes whitespace to be normalized when
-   comparing expected and actual output. This allows us to wrap long
-   lines out output:
-   >>> doctest: +NORMALIZE_WHITESPACE
-
 Interfaces are objects that specify (document) the external behavior
 of objects that "provide" them.  An interface specifies behavior
 through: 

Modified: Zope3/branches/ZopeX3-3.0/src/zope/interface/tests/test_interface.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/interface/tests/test_interface.py	2004-08-23 23:50:00 UTC (rev 27238)
+++ Zope3/branches/ZopeX3-3.0/src/zope/interface/tests/test_interface.py	2004-08-23 23:55:09 UTC (rev 27239)
@@ -273,11 +273,14 @@
 
 
 def test_suite():
-    from zope.testing.doctestunit import DocFileSuite
+    from zope.testing import doctest
     suite = unittest.makeSuite(InterfaceTests)
-    suite.addTest(DocTestSuite("zope.interface.interface"))
-    suite.addTest(DocFileSuite('../README.txt',
-                               globs={'__name__': '__main__'}))
+    suite.addTest(doctest.DocTestSuite("zope.interface.interface"))
+    suite.addTest(doctest.DocFileSuite(
+        '../README.txt',
+        globs={'__name__': '__main__'},
+        optionflags=doctest.NORMALIZE_WHITESPACE,
+        ))
     return suite
 
 def main():

Modified: Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py	2004-08-23 23:50:00 UTC (rev 27238)
+++ Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py	2004-08-23 23:55:09 UTC (rev 27239)
@@ -6,16 +6,15 @@
 
 # Provided as-is; use at your own risk; no warranty; no promises; enjoy!
 
-# [XX] This docstring is out-of-date:
 r"""Module doctest -- a framework for running examples in docstrings.
 
 NORMAL USAGE
 
-In normal use, end each module M with:
+In simplest use, end each module M to be tested with:
 
 def _test():
-    import doctest, M           # replace M with your module's name
-    return doctest.testmod(M)   # ditto
+    import doctest
+    return doctest.testmod()
 
 if __name__ == "__main__":
     _test()
@@ -37,15 +36,21 @@
 and a detailed report of all examples tried is printed to stdout, along
 with assorted summaries at the end.
 
-You can force verbose mode by passing "verbose=1" to testmod, or prohibit
-it by passing "verbose=0".  In either of those cases, sys.argv is not
+You can force verbose mode by passing "verbose=True" to testmod, or prohibit
+it by passing "verbose=False".  In either of those cases, sys.argv is not
 examined by testmod.
 
 In any case, testmod returns a 2-tuple of ints (f, t), where f is the
 number of docstring examples that failed and t is the total number of
 docstring examples attempted.
 
+There are a variety of other ways to run doctests, including integration
+with the unittest framework, and support for running non-Python text
+files containing doctests.  There are also many ways to override parts
+of doctest's default behaviors.  See the Library Reference Manual for
+details.
 
+
 WHICH DOCSTRINGS ARE EXAMINED?
 
 + M.__doc__.
@@ -59,28 +64,14 @@
 + If M.__test__ exists and "is true", it must be a dict, and
   each entry maps a (string) name to a function object, class object, or
   string.  Function and class object docstrings found from M.__test__
-  are searched even if the name is private, and strings are searched
-  directly as if they were docstrings.  In output, a key K in M.__test__
-  appears with name
+  are searched, and strings are searched directly as if they were docstrings.
+  In output, a key K in M.__test__ appears with name
       <name of M>.__test__.K
 
 Any classes found are recursively searched similarly, to test docstrings in
-their contained methods and nested classes.  All names reached from
-M.__test__ are searched.
+their contained methods and nested classes.
 
-Optionally, functions with private names can be skipped (unless listed in
-M.__test__) by supplying a function to the "isprivate" argument that will
-identify private functions.  For convenience, one such function is
-supplied.  docttest.is_private considers a name to be private if it begins
-with an underscore (like "_my_func") but doesn't both begin and end with
-(at least) two underscores (like "__init__").  By supplying this function
-or your own "isprivate" function to testmod, the behavior can be customized.
 
-If you want to test docstrings in objects with private names too, stuff
-them into an M.__test__ dict, or see ADVANCED USAGE below (e.g., pass your
-own isprivate function to Tester's constructor, or call the rundoc method
-of a Tester instance).
-
 WHAT'S THE EXECUTION CONTEXT?
 
 By default, each time testmod finds a docstring to test, it uses a *copy*
@@ -96,48 +87,6 @@
 M.__dict__ merged with the globals from other imported modules.
 
 
-WHAT IF I WANT TO TEST A WHOLE PACKAGE?
-
-Piece o' cake, provided the modules do their testing from docstrings.
-Here's the test.py I use for the world's most elaborate Rational/
-floating-base-conversion pkg (which I'll distribute some day):
-
-from Rational import Cvt
-from Rational import Format
-from Rational import machprec
-from Rational import Rat
-from Rational import Round
-from Rational import utils
-
-modules = (Cvt,
-           Format,
-           machprec,
-           Rat,
-           Round,
-           utils)
-
-def _test():
-    import doctest
-    import sys
-    verbose = "-v" in sys.argv
-    for mod in modules:
-        doctest.testmod(mod, verbose=verbose, report=0)
-    doctest.master.summarize()
-
-if __name__ == "__main__":
-    _test()
-
-IOW, it just runs testmod on all the pkg modules.  testmod remembers the
-names and outcomes (# of failures, # of tries) for each item it's seen, and
-passing "report=0" prevents it from printing a summary in verbose mode.
-Instead, the summary is delayed until all modules have been tested, and
-then "doctest.master.summarize()" forces the summary at the end.
-
-So this is very nice in practice:  each module can be tested individually
-with almost no work beyond writing up docstring examples, and collections
-of modules can be tested too as a unit with no more work than the above.
-
-
 WHAT ABOUT EXCEPTIONS?
 
 No problem, as long as the only output generated by the example is the
@@ -149,26 +98,11 @@
     ValueError: list.remove(x): x not in list
     >>>
 
-Note that only the exception type and value are compared (specifically,
-only the last line in the traceback).
+Note that only the exception type and value are compared.
 
 
-ADVANCED USAGE
+SO WHAT DOES A DOCTEST EXAMPLE LOOK LIKE ALREADY!?
 
-doctest.testmod() captures the testing policy I find most useful most
-often.  You may want other policies.
-
-testmod() actually creates a local instance of class doctest.Tester, runs
-appropriate methods of that class, and merges the results into global
-Tester instance doctest.master.
-
-You can create your own instances of doctest.Tester, and so build your own
-policies, or even run methods of doctest.master directly.  See
-doctest.Tester.__doc__ for details.
-
-
-SO WHAT DOES A DOCSTRING EXAMPLE LOOK LIKE ALREADY!?
-
 Oh ya.  It's easy!  In most cases a copy-and-paste of an interactive
 console session works fine -- just make sure the leading whitespace is
 rigidly consistent (you can mix tabs and spaces if you're too lazy to do it
@@ -197,9 +131,6 @@
 
 Bummers:
 
-+ Expected output cannot contain an all-whitespace line, since such a line
-  is taken to signal the end of expected output.
-
 + Output to stdout is captured, but not output to stderr (exception
   tracebacks are captured via a different means).
 
@@ -234,103 +165,62 @@
 output as appeared in the initial ">>>" line that triggered it.
 
 If you execute this very file, the examples above will be found and
-executed, leading to this output in verbose mode:
-
-Running doctest.__doc__
-Trying: [1, 2, 3].remove(42)
-Expecting:
-Traceback (most recent call last):
-  File "<stdin>", line 1, in ?
-ValueError: list.remove(x): x not in list
-ok
-Trying: x = 12
-Expecting: nothing
-ok
-Trying: x
-Expecting: 12
-ok
-Trying:
-if x == 13:
-    print "yes"
-else:
-    print "no"
-    print "NO"
-    print "NO!!!"
-Expecting:
-no
-NO
-NO!!!
-ok
-... and a bunch more like that, with this summary at the end:
-
-5 items had no tests:
-    doctest.Tester.__init__
-    doctest.Tester.run__test__
-    doctest.Tester.summarize
-    doctest.run_docstring_examples
-    doctest.testmod
-12 items passed all tests:
-   8 tests in doctest
-   6 tests in doctest.Tester
-  10 tests in doctest.Tester.merge
-  14 tests in doctest.Tester.rundict
-   3 tests in doctest.Tester.rundoc
-   3 tests in doctest.Tester.runstring
-   2 tests in doctest.__test__._TestClass
-   2 tests in doctest.__test__._TestClass.__init__
-   2 tests in doctest.__test__._TestClass.get
-   1 tests in doctest.__test__._TestClass.square
-   2 tests in doctest.__test__.string
-   7 tests in doctest.is_private
-60 tests in 17 items.
-60 passed and 0 failed.
-Test passed.
+executed.
 """
+__docformat__ = 'reStructuredText en'
 
 __all__ = [
+    # 0, Option Flags
+    'register_optionflag',
+    'DONT_ACCEPT_TRUE_FOR_1',
+    'DONT_ACCEPT_BLANKLINE',
+    'NORMALIZE_WHITESPACE',
+    'ELLIPSIS',
+    'UNIFIED_DIFF',
+    'CONTEXT_DIFF',
+    # 1. Utility Functions
     'is_private',
+    # 2. Example & DocTest
     'Example',
     'DocTest',
+    # 3. Doctest Parser
+    'DocTestParser',
+    # 4. Doctest Finder
     'DocTestFinder',
+    # 5. Doctest Runner
     'DocTestRunner',
+    'OutputChecker',
+    'DocTestFailure',
+    'UnexpectedException',
+    'DebugRunner',
+    # 6. Test Functions
     'testmod',
     'run_docstring_examples',
+    # 7. Tester
     'Tester',
+    # 8. Unittest Support
     'DocTestCase',
     'DocTestSuite',
+    'DocFileCase',
+    'DocFileTest',
+    'DocFileSuite',
+    # 9. Debugging Support
+    'script_from_examples',
     'testsource',
+    'debug_src',
+    'debug_script',
     'debug',
-#    'master',
 ]
 
 import __future__
 
 import sys, traceback, inspect, linecache, os, re, types
-import unittest, difflib, tempfile
+import unittest, difflib, pdb, tempfile
+import warnings
 from StringIO import StringIO
 
-# Option constants.
-DONT_ACCEPT_TRUE_FOR_1 = 1 << 0
-DONT_ACCEPT_BLANKLINE = 1 << 1
-NORMALIZE_WHITESPACE = 1 << 2
-ELLIPSIS = 1 << 3
-UNIFIED_DIFF = 1 << 4
-CONTEXT_DIFF = 1 << 5
+real_pdb_set_trace = pdb.set_trace
 
-OPTIONFLAGS_BY_NAME = {
-    'DONT_ACCEPT_TRUE_FOR_1': DONT_ACCEPT_TRUE_FOR_1,
-    'DONT_ACCEPT_BLANKLINE': DONT_ACCEPT_BLANKLINE,
-    'NORMALIZE_WHITESPACE': NORMALIZE_WHITESPACE,
-    'ELLIPSIS': ELLIPSIS,
-    'UNIFIED_DIFF': UNIFIED_DIFF,
-    'CONTEXT_DIFF': CONTEXT_DIFF,
-    }
-
-# Special string markers for use in `want` strings:
-BLANKLINE_MARKER = '<BLANKLINE>'
-ELLIPSIS_MARKER = '...'
-
-
 # There are 4 basic classes:
 #  - Example: a <source, want> pair, plus an intra-docstring line number.
 #  - DocTest: a collection of examples, parsed from a docstring, plus
@@ -350,18 +240,37 @@
 #                            | Example |
 #                            +---------+
 
+# Option constants.
+OPTIONFLAGS_BY_NAME = {}
+def register_optionflag(name):
+    flag = 1 << len(OPTIONFLAGS_BY_NAME)
+    OPTIONFLAGS_BY_NAME[name] = flag
+    return flag
+
+DONT_ACCEPT_TRUE_FOR_1 = register_optionflag('DONT_ACCEPT_TRUE_FOR_1')
+DONT_ACCEPT_BLANKLINE = register_optionflag('DONT_ACCEPT_BLANKLINE')
+NORMALIZE_WHITESPACE = register_optionflag('NORMALIZE_WHITESPACE')
+ELLIPSIS = register_optionflag('ELLIPSIS')
+UNIFIED_DIFF = register_optionflag('UNIFIED_DIFF')
+CONTEXT_DIFF = register_optionflag('CONTEXT_DIFF')
+
+# Special string markers for use in `want` strings:
+BLANKLINE_MARKER = '<BLANKLINE>'
+ELLIPSIS_MARKER = '...'
+
 ######################################################################
 ## Table of Contents
 ######################################################################
-# 1. Utility Functions
-# 2. Example & DocTest -- store test cases
-# 3. DocTest Finder -- extracts test cases from objects
-# 4. DocTest Runner -- runs test cases
-# 5. Test Functions -- convenient wrappers for testing
-# 6. Tester Class -- for backwards compatibility
-# 7. Unittest Support
-# 8. Debugging Support
-# 9. Example Usage
+#  1. Utility Functions
+#  2. Example & DocTest -- store test cases
+#  3. DocTest Parser -- extracts examples from strings
+#  4. DocTest Finder -- extracts test cases from objects
+#  5. DocTest Runner -- runs test cases
+#  6. Test Functions -- convenient wrappers for testing
+#  7. Tester Class -- for backwards compatibility
+#  8. Unittest Support
+#  9. Debugging Support
+# 10. Example Usage
 
 ######################################################################
 ## 1. Utility Functions
@@ -376,6 +285,8 @@
     Return true iff base begins with an (at least one) underscore, but
     does not both begin and end with (at least) two underscores.
 
+    >>> warnings.filterwarnings("ignore", "is_private", DeprecationWarning,
+    ...                         "doctest", 0)
     >>> is_private("a.b", "my_func")
     False
     >>> is_private("____", "_my_func")
@@ -391,6 +302,9 @@
     >>> is_private("", "")  # senseless but consistent
     False
     """
+    warnings.warn("is_private is deprecated; it wasn't useful; "
+                  "examine DocTestFinder.find() lists instead",
+                  DeprecationWarning, stacklevel=2)
     return base[:1] == "_" and not base[:2] == "__" == base[-2:]
 
 def _extract_future_flags(globs):
@@ -424,16 +338,13 @@
     else:
         raise TypeError("Expected a module, string, or None")
 
-def _tag_msg(tag, msg, indent_msg=True):
+def _tag_msg(tag, msg, indent='    '):
     """
     Return a string that displays a tag-and-message pair nicely,
     keeping the tag and its message on the same line when that
-    makes sense.  If `indent_msg` is true, then messages that are
-    put on separate lines will be indented.
+    makes sense.  If the message is displayed on separate lines,
+    then `indent` is added to the beginning of each line.
     """
-    # What string should we use to indent contents?
-    INDENT = '    '
-
     # If the message doesn't end in a newline, then add one.
     if msg[-1:] != '\n':
         msg += '\n'
@@ -444,11 +355,20 @@
         msg.find('\n', 0, len(msg)-1) == -1):
         return '%s: %s' % (tag, msg)
     else:
-        if indent_msg:
-            msg = '\n'.join([INDENT+l for l in msg.split('\n')])
-            msg = msg[:-len(INDENT)]
-        return '%s:\n%s' % (tag, msg)
+        msg = '\n'.join([indent+l for l in msg[:-1].split('\n')])
+        return '%s:\n%s\n' % (tag, msg)
 
+def _exception_traceback(exc_info):
+    """
+    Return a string containing a traceback message for the given
+    exc_info tuple (as returned by sys.exc_info()).
+    """
+    # Get a traceback message.
+    excout = StringIO()
+    exc_type, exc_val, exc_tb = exc_info
+    traceback.print_exception(exc_type, exc_val, exc_tb, file=excout)
+    return excout.getvalue()
+
 # Override some StringIO methods.
 class _SpoofOut(StringIO):
     def getvalue(self):
@@ -469,44 +389,218 @@
         if hasattr(self, "softspace"):
             del self.softspace
 
-class Parser:
+# Worst-case linear-time ellipsis matching.
+def _ellipsis_match(want, got):
     """
-    Extract doctests from a string.
+    Essentially the only subtle case:
+    >>> _ellipsis_match('aa...aa', 'aaa')
+    False
     """
+    if ELLIPSIS_MARKER not in want:
+        return want == got
 
-    _PS1 = ">>>"
-    _PS2 = "..."
-    _isPS1 = re.compile(r"(\s*)" + re.escape(_PS1)).match
-    _isPS2 = re.compile(r"(\s*)" + re.escape(_PS2)).match
-    _isEmpty = re.compile(r"\s*$").match
-    _isComment = re.compile(r"\s*#").match
+    # Find "the real" strings.
+    ws = want.split(ELLIPSIS_MARKER)
+    assert len(ws) >= 2
 
-    def __init__(self, name, string):
-        """
-        Prepare to extract doctests from string `string`.
+    # Deal with exact matches possibly needed at one or both ends.
+    startpos, endpos = 0, len(got)
+    w = ws[0]
+    if w:   # starts with exact match
+        if got.startswith(w):
+            startpos = len(w)
+            del ws[0]
+        else:
+            return False
+    w = ws[-1]
+    if w:   # ends with exact match
+        if got.endswith(w):
+            endpos -= len(w)
+            del ws[-1]
+        else:
+            return False
 
-        `name` is an arbitrary (string) name associated with the string,
-        and is used only in error messages.
+    if startpos > endpos:
+        # Exact end matches required more characters than we have, as in
+        # _ellipsis_match('aa...aa', 'aaa')
+        return False
+
+    # For the rest, we only need to find the leftmost non-overlapping
+    # match for each piece.  If there's no overall match that way alone,
+    # there's no overall match period.
+    for w in ws:
+        # w may be '' at times, if there are consecutive ellipses, or
+        # due to an ellipsis at the start or end of `want`.  That's OK.
+        # Search for an empty string succeeds, and doesn't change startpos.
+        startpos = got.find(w, startpos, endpos)
+        if startpos < 0:
+            return False
+        startpos += len(w)
+
+    return True
+
+######################################################################
+## 2. Example & DocTest
+######################################################################
+## - An "example" is a <source, want> pair, where "source" is a
+##   fragment of source code, and "want" is the expected output for
+##   "source."  The Example class also includes information about
+##   where the example was extracted from.
+##
+## - A "doctest" is a collection of examples, typically extracted from
+##   a string (such as an object's docstring).  The DocTest class also
+##   includes information about where the string was extracted from.
+
+class Example:
+    """
+    A single doctest example, consisting of source code and expected
+    output.  `Example` defines the following attributes:
+
+      - source: A single Python statement, always ending with a newline.
+        The constructor adds a newline if needed.
+
+      - want: The expected output from running the source code (either
+        from stdout, or a traceback in case of exception).  `want` ends
+        with a newline unless it's empty, in which case it's an empty
+        string.  The constructor adds a newline if needed.
+
+      - lineno: The line number within the DocTest string containing
+        this Example where the Example begins.  This line number is
+        zero-based, with respect to the beginning of the DocTest.
+
+      - indent: The example's indentation in the DocTest string.
+        I.e., the number of space characters that preceed the
+        example's first prompt.
+
+      - options: A dictionary mapping from option flags to True or
+        False, which is used to override default options for this
+        example.  Any option flags not contained in this dictionary
+        are left at their default value (as specified by the
+        DocTestRunner's optionflags).  By default, no options are set.
+    """
+    def __init__(self, source, want, lineno, indent=0, options=None):
+        # Normalize inputs.
+        if not source.endswith('\n'):
+            source += '\n'
+        if want and not want.endswith('\n'):
+            want += '\n'
+        # Store properties.
+        self.source = source
+        self.want = want
+        self.lineno = lineno
+        self.indent = indent
+        if options is None: options = {}
+        self.options = options
+
+class DocTest:
+    """
+    A collection of doctest examples that should be run in a single
+    namespace.  Each `DocTest` defines the following attributes:
+
+      - examples: the list of examples.
+
+      - globs: The namespace (aka globals) that the examples should
+        be run in.
+
+      - name: A name identifying the DocTest (typically, the name of
+        the object whose docstring this DocTest was extracted from).
+
+      - filename: The name of the file that this DocTest was extracted
+        from, or `None` if the filename is unknown.
+
+      - lineno: The line number within filename where this DocTest
+        begins, or `None` if the line number is unavailable.  This
+        line number is zero-based, with respect to the beginning of
+        the file.
+
+      - docstring: The string that the examples were extracted from,
+        or `None` if the string is unavailable.
+    """
+    def __init__(self, examples, globs, name, filename, lineno, docstring):
         """
+        Create a new DocTest containing the given examples.  The
+        DocTest's globals are initialized with a copy of `globs`.
+        """
+        assert not isinstance(examples, basestring), \
+               "DocTest no longer accepts str; use DocTestParser instead"
+        self.examples = examples
+        self.docstring = docstring
+        self.globs = globs.copy()
         self.name = name
-        self.source = string
+        self.filename = filename
+        self.lineno = lineno
 
-    def get_examples(self):
+    def __repr__(self):
+        if len(self.examples) == 0:
+            examples = 'no examples'
+        elif len(self.examples) == 1:
+            examples = '1 example'
+        else:
+            examples = '%d examples' % len(self.examples)
+        return ('<DocTest %s from %s:%s (%s)>' %
+                (self.name, self.filename, self.lineno, examples))
+
+
+    # This lets us sort tests by name:
+    def __cmp__(self, other):
+        if not isinstance(other, DocTest):
+            return -1
+        return cmp((self.name, self.filename, self.lineno, id(self)),
+                   (other.name, other.filename, other.lineno, id(other)))
+
+######################################################################
+## 3. DocTestParser
+######################################################################
+
+class DocTestParser:
+    """
+    A class used to parse strings containing doctest examples.
+    """
+    # This regular expression is used to find doctest examples in a
+    # string.  It defines three groups: `source` is the source code
+    # (including leading indentation and prompts); `indent` is the
+    # indentation of the first (PS1) line of the source code; and
+    # `want` is the expected output (including leading indentation).
+    _EXAMPLE_RE = re.compile(r'''
+        # Source consists of a PS1 line followed by zero or more PS2 lines.
+        (?P<source>
+            (?:^(?P<indent> [ ]*) >>>    .*)    # PS1 line
+            (?:\n           [ ]*  \.\.\. .*)*)  # PS2 lines
+        \n?
+        # Want consists of any non-blank lines that do not start with PS1.
+        (?P<want> (?:(?![ ]*$)    # Not a blank line
+                     (?![ ]*>>>)  # Not a line starting with PS1
+                     .*$\n?       # But any other line
+                  )*)
+        ''', re.MULTILINE | re.VERBOSE)
+
+    # A callable returning a true value iff its argument is a blank line
+    # or contains a single comment.
+    _IS_BLANK_OR_COMMENT = re.compile(r'^[ ]*(#.*)?$').match
+
+    def get_doctest(self, string, globs, name, filename, lineno):
         """
-        Return the doctest examples from the string.
+        Extract all doctest examples from the given string, and
+        collect them into a `DocTest` object.
 
-        This is a list of (source, want, lineno) triples, one per example
-        in the string.  "source" is a single Python statement; it ends
-        with a newline iff the statement contains more than one
-        physical line.  "want" is the expected output from running the
-        example (either from stdout, or a traceback in case of exception).
-        "want" always ends with a newline, unless no output is expected,
-        in which case "want" is an empty string.  "lineno" is the 0-based
-        line number of the first line of "source" within the string.  It's
-        0-based because it's most common in doctests that nothing
+        `globs`, `name`, `filename`, and `lineno` are attributes for
+        the new `DocTest` object.  See the documentation for `DocTest`
+        for more information.
+        """
+        return DocTest(self.get_examples(string, name), globs,
+                       name, filename, lineno, string)
+
+    def get_examples(self, string, name='<string>'):
+        """
+        Extract all doctest examples from the given string, and return
+        them as a list of `Example` objects.  Line numbers are
+        0-based, because it's most common in doctests that nothing
         interesting appears on the same line as opening triple-quote,
-        and so the first interesting line is called "line 1" then.
+        and so the first interesting line is called \"line 1\" then.
 
+        The optional argument `name` is a name identifying this
+        string, and is only used for error messages.
+
         >>> text = '''
         ...        >>> x, y = 2, 3  # no output expected
         ...        >>> if 1:
@@ -519,24 +613,45 @@
         ...        >>> x+y
         ...        5
         ...        '''
-        >>> for x in Parser('<string>', text).get_examples():
-        ...     print x
-        ('x, y = 2, 3  # no output expected', '', 1)
+        >>> for x in DocTestParser().get_examples(text):
+        ...     print (x.source, x.want, x.lineno)
+        ('x, y = 2, 3  # no output expected\\n', '', 1)
         ('if 1:\\n    print x\\n    print y\\n', '2\\n3\\n', 2)
-        ('x+y', '5\\n', 9)
+        ('x+y\\n', '5\\n', 9)
         """
-        return self._parse(kind='examples')
+        examples = []
+        charno, lineno = 0, 0
+        # Find all doctest examples in the string:
+        for m in self._EXAMPLE_RE.finditer(string.expandtabs()):
+            # Update lineno (lines before this example)
+            lineno += string.count('\n', charno, m.start())
+            # Extract source/want from the regexp match.
+            (source, want) = self._parse_example(m, name, lineno)
+            # Extract extra options from the source.
+            options = self._find_options(source, name, lineno)
+            # Create an Example, and add it to the list.
+            if not self._IS_BLANK_OR_COMMENT(source):
+                examples.append( Example(source, want, lineno,
+                                         len(m.group('indent')), options) )
+            # Update lineno (lines inside this example)
+            lineno += string.count('\n', m.start(), m.end())
+            # Update charno.
+            charno = m.end()
+        return examples
 
-    def get_program(self):
+    def get_program(self, string, name="<string>"):
         """
-        Return an executable program from the string, as a string.
+        Return an executable program from the given string, as a string.
 
         The format of this isn't rigidly defined.  In general, doctest
         examples become the executable statements in the result, and
-        their expected outputs become comments, preceded by an "#Expected:"
+        their expected outputs become comments, preceded by an \"#Expected:\"
         comment.  Everything else (text, comments, everything not part of
         a doctest test) is also placed in comments.
 
+        The optional argument `name` is a name identifying this
+        string, and is only used for error messages.
+
         >>> text = '''
         ...        >>> x, y = 2, 3  # no output expected
         ...        >>> if 1:
@@ -549,223 +664,177 @@
         ...        >>> x+y
         ...        5
         ...        '''
-        >>> print Parser('<string>', text).get_program()
+        >>> print DocTestParser().get_program(text)
         x, y = 2, 3  # no output expected
         if 1:
             print x
             print y
         # Expected:
-        #     2
-        #     3
+        ## 2
+        ## 3
         #
-        #         Some text.
+        # Some text.
         x+y
         # Expected:
-        #     5
+        ## 5
         """
-        return self._parse(kind='program')
+        string = string.expandtabs()
+        # If all lines begin with the same indentation, then strip it.
+        min_indent = self._min_indent(string)
+        if min_indent > 0:
+            string = '\n'.join([l[min_indent:] for l in string.split('\n')])
 
-    def _parse(self,   kind):
-        assert kind in ('examples', 'program')
-        do_program = kind == 'program'
         output = []
-        push = output.append
+        charnum, lineno = 0, 0
+        # Find all doctest examples in the string:
+        for m in self._EXAMPLE_RE.finditer(string.expandtabs()):
+            # Add any text before this example, as a comment.
+            if m.start() > charnum:
+                lines = string[charnum:m.start()-1].split('\n')
+                output.extend([self._comment_line(l) for l in lines])
+                lineno += len(lines)
 
-        string = self.source
-        if not string.endswith('\n'):
-            string += '\n'
+            # Extract source/want from the regexp match.
+            (source, want) = self._parse_example(m, name, lineno)
+            # Display the source
+            output.append(source)
+            # Display the expected output, if any
+            if want:
+                output.append('# Expected:')
+                output.extend(['## '+l for l in want.split('\n')])
 
-        isPS1, isPS2 = self._isPS1, self._isPS2
-        isEmpty, isComment = self._isEmpty, self._isComment
-        lines = string.split("\n")
-        i, n = 0, len(lines)
-        while i < n:
-            # Search for an example (a PS1 line).
-            line = lines[i]
-            i += 1
-            m = isPS1(line)
-            if m is None:
-                if do_program:
-                    line = line.rstrip()
-                    if line:
-                        line = '  ' + line
-                    push('#' + line)
-                continue
-            # line is a PS1 line.
-            j = m.end(0)  # beyond the prompt
-            if isEmpty(line, j) or isComment(line, j):
-                # a bare prompt or comment -- not interesting
-                if do_program:
-                    push("#  " + line[j:])
-                continue
-            # line is a non-trivial PS1 line.
-            lineno = i - 1
-            if line[j] != " ":
-                raise ValueError('line %r of the docstring for %s lacks '
-                                 'blank after %s: %r' %
-                                 (lineno, self.name, self._PS1, line))
+            # Update the line number & char number.
+            lineno += string.count('\n', m.start(), m.end())
+            charnum = m.end()
+        # Add any remaining text, as comments.
+        output.extend([self._comment_line(l)
+                       for l in string[charnum:].split('\n')])
+        # Trim junk on both ends.
+        while output and output[-1] == '#':
+            output.pop()
+        while output and output[0] == '#':
+            output.pop(0)
+        # Combine the output, and return it.
+        return '\n'.join(output)
 
-            j += 1
-            blanks = m.group(1)
-            nblanks = len(blanks)
-            # suck up this and following PS2 lines
-            source = []
-            while 1:
-                source.append(line[j:])
-                line = lines[i]
-                m = isPS2(line)
-                if m:
-                    if m.group(1) != blanks:
-                        raise ValueError('line %r of the docstring for %s '
-                            'has inconsistent leading whitespace: %r' %
-                            (i, self.name, line))
-                    i += 1
-                else:
-                    break
+    def _parse_example(self, m, name, lineno):
+        """
+        Given a regular expression match from `_EXAMPLE_RE` (`m`),
+        return a pair `(source, want)`, where `source` is the matched
+        example's source code (with prompts and indentation stripped);
+        and `want` is the example's expected output (with indentation
+        stripped).
 
-            if do_program:
-                output.extend(source)
-            else:
-                # get rid of useless null line from trailing empty "..."
-                if source[-1] == "":
-                    assert len(source) > 1
-                    del source[-1]
-                if len(source) == 1:
-                    source = source[0]
-                else:
-                    source = "\n".join(source) + "\n"
+        `name` is the string's name, and `lineno` is the line number
+        where the example starts; both are used for error messages.
+        """
+        # Get the example's indentation level.
+        indent = len(m.group('indent'))
 
-            # suck up response
-            if isPS1(line) or isEmpty(line):
-                if not do_program:
-                    push((source, "", lineno))
-                continue
+        # Divide source into lines; check that they're properly
+        # indented; and then strip their indentation & prompts.
+        source_lines = m.group('source').split('\n')
+        self._check_prompt_blank(source_lines, indent, name, lineno)
+        self._check_prefix(source_lines[1:], ' '*indent+'.', name, lineno)
+        source = '\n'.join([sl[indent+4:] for sl in source_lines])
 
-            # There is a response.
-            want = []
-            if do_program:
-                push("# Expected:")
-            while 1:
-                if line[:nblanks] != blanks:
-                    raise ValueError('line %r of the docstring for %s '
-                        'has inconsistent leading whitespace: %r' %
-                        (i, self.name, line))
-                want.append(line[nblanks:])
-                i += 1
-                line = lines[i]
-                if isPS1(line) or isEmpty(line):
-                    break
+        # Divide want into lines; check that it's properly
+        # indented; and then strip the indentation.
+        want = m.group('want')
 
-            if do_program:
-                output.extend(['#     ' + x for x in want])
-            else:
-                want = "\n".join(want) + "\n"
-                push((source, want, lineno))
+        # Strip trailing newline and following spaces
+        l = len(want.rstrip())
+        l = want.find('\n', l)
+        if l >= 0:
+            want = want[:l]
+            
+        want_lines = want.split('\n')
+        self._check_prefix(want_lines, ' '*indent, name,
+                           lineno+len(source_lines))
+        want = '\n'.join([wl[indent:] for wl in want_lines])
 
-        if do_program:
-            # Trim junk on both ends.
-            while output and output[-1] == '#':
-                output.pop()
-            while output and output[0] == '#':
-                output.pop(0)
-            output = '\n'.join(output)
+        return source, want
 
-        return output
+    # This regular expression looks for option directives in the
+    # source code of an example.  Option directives are comments
+    # starting with "doctest:".  Warning: this may give false
+    # positives for string-literals that contain the string
+    # "#doctest:".  Eliminating these false positives would require
+    # actually parsing the string; but we limit them by ignoring any
+    # line containing "#doctest:" that is *followed* by a quote mark.
+    _OPTION_DIRECTIVE_RE = re.compile(r'#\s*doctest:\s*([^\n\'"]*)$',
+                                      re.MULTILINE)
 
-######################################################################
-## 2. Example & DocTest
-######################################################################
-## - An "example" is a <source, want> pair, where "source" is a
-##   fragment of source code, and "want" is the expected output for
-##   "source."  The Example class also includes information about
-##   where the example was extracted from.
-##
-## - A "doctest" is a collection of examples extracted from a string
-##   (such as an object's docstring).  The DocTest class also includes
-##   information about where the string was extracted from.
+    def _find_options(self, source, name, lineno):
+        """
+        Return a dictionary containing option overrides extracted from
+        option directives in the given source string.
 
-class Example:
-    """
-    A single doctest example, consisting of source code and expected
-    output.  Example defines the following attributes:
+        `name` is the string's name, and `lineno` is the line number
+        where the example starts; both are used for error messages.
+        """
+        options = {}
+        # (note: with the current regexp, this will match at most once:)
+        for m in self._OPTION_DIRECTIVE_RE.finditer(source):
+            option_strings = m.group(1).replace(',', ' ').split()
+            for option in option_strings:
+                if (option[0] not in '+-' or
+                    option[1:] not in OPTIONFLAGS_BY_NAME):
+                    raise ValueError('line %r of the doctest for %s '
+                                     'has an invalid option: %r' %
+                                     (lineno+1, name, option))
+                flag = OPTIONFLAGS_BY_NAME[option[1:]]
+                options[flag] = (option[0] == '+')
+        if options and self._IS_BLANK_OR_COMMENT(source):
+            raise ValueError('line %r of the doctest for %s has an option '
+                             'directive on a line with no example: %r' %
+                             (lineno, name, source))
+        return options
 
-      - source: The source code that should be run.  It ends with a
-        newline iff the source spans more than one line.
+    # This regular expression finds the indentation of every non-blank
+    # line in a string.
+    _INDENT_RE = re.compile('^([ ]+)(?=\S)', re.MULTILINE)
 
-      - want: The expected output from running the source code.  If
-        not empty, then this string ends with a newline.
+    def _min_indent(self, s):
+        "Return the minimum indentation of any non-blank line in `s`"
+        return min([len(indent) for indent in self._INDENT_RE.findall(s)])
 
-      - lineno: The line number within the DocTest string containing
-        this Example where the Example begins.  This line number is
-        zero-based, with respect to the beginning of the DocTest.
-    """
-    def __init__(self, source, want, lineno):
-        # Check invariants.
-        assert (source[-1:] == '\n') == ('\n' in source[:-1])
-        assert want == '' or want[-1] == '\n'
-        # Store properties.
-        self.source = source
-        self.want = want
-        self.lineno = lineno
+    def _comment_line(self, line):
+        "Return a commented form of the given line"
+        line = line.rstrip()
+        if line:
+            return '# '+line
+        else:
+            return '#'
 
-class DocTest:
-    """
-    A collection of doctest examples that should be run in a single
-    namespace.  Each DocTest defines the following attributes:
+    def _check_prompt_blank(self, lines, indent, name, lineno):
+        """
+        Given the lines of a source string (including prompts and
+        leading indentation), check to make sure that every prompt is
+        followed by a space character.  If any line is not followed by
+        a space character, then raise ValueError.
+        """
+        for i, line in enumerate(lines):
+            if len(line) >= indent+4 and line[indent+3] != ' ':
+                raise ValueError('line %r of the docstring for %s '
+                                 'lacks blank after %s: %r' %
+                                 (lineno+i+1, name,
+                                  line[indent:indent+3], line))
 
-      - examples: the list of examples.
-
-      - globs: The namespace (aka globals) that the examples should
-        be run in.
-
-      - name: A name identifying the DocTest (typically, the name of
-        the object whose docstring this DocTest was extracted from).
-
-      - docstring: The docstring being tested
-
-      - filename: The name of the file that this DocTest was extracted
-        from.
-
-      - lineno: The line number within filename where this DocTest
-        begins.  This line number is zero-based, with respect to the
-        beginning of the file.
-    """
-    def __init__(self, docstring, globs, name, filename, lineno):
+    def _check_prefix(self, lines, prefix, name, lineno):
         """
-        Create a new DocTest, by extracting examples from `docstring`.
-        The DocTest's globals are initialized with a copy of `globs`.
+        Check that every line in the given list starts with the given
+        prefix; if any line does not, then raise a ValueError.
         """
-        # Store a copy of the globals
-        self.globs = globs.copy()
-        # Store identifying information
-        self.name = name
-        self.filename = filename
-        self.lineno = lineno
-        # Parse the docstring.
-        self.docstring = docstring
-        examples = Parser(name, docstring).get_examples()
-        self.examples = [Example(*example) for example in examples]
+        for i, line in enumerate(lines):
+            if line and not line.startswith(prefix):
+                raise ValueError('line %r of the docstring for %s has '
+                                 'inconsistent leading whitespace: %r' %
+                                 (lineno+i+1, name, line))
 
-    def __repr__(self):
-        if len(self.examples) == 0:
-            examples = 'no examples'
-        elif len(self.examples) == 1:
-            examples = '1 example'
-        else:
-            examples = '%d examples' % len(self.examples)
-        return ('<DocTest %s from %s:%s (%s)>' %
-                (self.name, self.filename, self.lineno, examples))
 
-
-    # This lets us sort tests by name:
-    def __cmp__(self, other):
-        if not isinstance(other, DocTest):
-            return -1
-        return cmp((self.name, self.filename, self.lineno, id(self)),
-                   (other.name, other.filename, other.lineno, id(other)))
-
 ######################################################################
-## 3. DocTest Finder
+## 4. DocTest Finder
 ######################################################################
 
 class DocTestFinder:
@@ -775,63 +844,56 @@
     objects.  Doctests can currently be extracted from the following
     object types: modules, functions, classes, methods, staticmethods,
     classmethods, and properties.
-
-    An optional name filter and an optional object filter may be
-    passed to the constructor, to restrict which contained objects are
-    examined by the doctest finder:
-
-      - The name filter is a function `f(prefix, base)`, that returns
-        true if an object named `prefix.base` should be ignored.
-      - The object filter is a function `f(obj)` that returns true
-        if the given object should be ignored.
-
-    Each object is ignored if either filter function returns true for
-    that object.  These filter functions are applied when examining
-    the contents of a module or of a class, but not when examining a
-    module's `__test__` dictionary.  By default, no objects are
-    ignored.
     """
 
-    def __init__(self, verbose=False, doctest_factory=DocTest,
-                 namefilter=None, objfilter=None, recurse=True):
+    def __init__(self, verbose=False, parser=DocTestParser(),
+                 recurse=True, _namefilter=None):
         """
         Create a new doctest finder.
 
-        The optional argument `doctest_factory` specifies a class or
+        The optional argument `parser` specifies a class or
         function that should be used to create new DocTest objects (or
-        objects that implement the same interface as DocTest).  This
+        objects that implement the same interface as DocTest).  The
         signature for this factory function should match the signature
         of the DocTest constructor.
 
         If the optional argument `recurse` is false, then `find` will
         only examine the given object, and not any contained objects.
         """
-        self._doctest_factory = doctest_factory
+        self._parser = parser
         self._verbose = verbose
-        self._namefilter = namefilter
-        self._objfilter = objfilter
         self._recurse = recurse
+        # _namefilter is undocumented, and exists only for temporary backward-
+        # compatibility support of testmod's deprecated isprivate mess.
+        self._namefilter = _namefilter
 
     def find(self, obj, name=None, module=None, globs=None,
-             extraglobs=None, ignore_imports=True):
+             extraglobs=None):
         """
         Return a list of the DocTests that are defined by the given
         object's docstring, or by any of its contained objects'
         docstrings.
 
         The optional parameter `module` is the module that contains
-        the given object.  If the module is not specified, then the
-        test finder will attempt to automatically determine the
+        the given object.  If the module is not specified or is None, then
+        the test finder will attempt to automatically determine the
         correct module.  The object's module is used:
 
             - As a default namespace, if `globs` is not specified.
             - To prevent the DocTestFinder from extracting DocTests
-              from objects that are imported from other modules
-              (as long as `ignore_imports` is true).
+              from objects that are imported from other modules.
             - To find the name of the file containing the object.
             - To help find the line number of the object within its
               file.
 
+        Contained objects whose module does not match `module` are ignored.
+
+        If `module` is False, no attempt to find the module will be made.
+        This is obscure, of use mostly in tests:  if `module` is False, or
+        is None but cannot be found automatically, then all objects are
+        considered to belong to the (non-existent) module, so all contained
+        objects will (recursively) be searched for doctests.
+
         The globals for each DocTest is formed by combining `globs`
         and `extraglobs` (bindings in `extraglobs` override bindings
         in `globs`).  A new copy of the globals dictionary is created
@@ -840,10 +902,6 @@
         otherwise.  If `extraglobs` is not specified, then it defaults
         to {}.
 
-        If the optional flag `ignore_imports` is true, then the
-        doctest finder will ignore any contained objects whose module
-        does not match `module`.  Otherwise, it will extract tests
-        from all contained objects, including imported objects.
         """
         # If name was not specified, then extract it from the object.
         if name is None:
@@ -856,7 +914,9 @@
         # Find the module that contains the given object (if obj is
         # a module, then module=obj.).  Note: this may fail, in which
         # case module will be None.
-        if module is None:
+        if module is False:
+            module = None
+        elif module is None:
             module = inspect.getmodule(obj)
 
         # Read the module's source code.  This is used by
@@ -883,18 +943,15 @@
 
         # Recursively expore `obj`, extracting DocTests.
         tests = []
-        self._find(tests, obj, name, module, source_lines,
-                   globs, ignore_imports, {})
+        self._find(tests, obj, name, module, source_lines, globs, {})
         return tests
 
     def _filter(self, obj, prefix, base):
         """
         Return true if the given object should not be examined.
         """
-        return ((self._namefilter is not None and
-                 self._namefilter(prefix, base)) or
-                (self._objfilter is not None and
-                 self._objfilter(obj)))
+        return (self._namefilter is not None and
+                self._namefilter(prefix, base))
 
     def _from_module(self, module, object):
         """
@@ -916,8 +973,7 @@
         else:
             raise ValueError("object must be a class or function")
 
-    def _find(self, tests, obj, name, module, source_lines,
-              globs, ignore_imports, seen):
+    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`.
@@ -944,9 +1000,9 @@
                 valname = '%s.%s' % (name, valname)
                 # Recurse to functions & classes.
                 if ((inspect.isfunction(val) or inspect.isclass(val)) and
-                    (self._from_module(module, val) or not ignore_imports)):
+                    self._from_module(module, val)):
                     self._find(tests, val, valname, module, source_lines,
-                               globs, ignore_imports, seen)
+                               globs, seen)
 
         # Look for tests in a module's __test__ dictionary.
         if inspect.ismodule(obj) and self._recurse:
@@ -964,7 +1020,7 @@
                                      (type(val),))
                 valname = '%s.%s' % (name, valname)
                 self._find(tests, val, valname, module, source_lines,
-                           globs, ignore_imports, seen)
+                           globs, seen)
 
         # Look for tests in a class's contained objects.
         if inspect.isclass(obj) and self._recurse:
@@ -980,11 +1036,11 @@
 
                 # Recurse to methods, properties, and nested classes.
                 if ((inspect.isfunction(val) or inspect.isclass(val) or
-                    isinstance(val, property)) and
-                    (self._from_module(module, val) or not ignore_imports)):
+                      isinstance(val, property)) and
+                      self._from_module(module, val)):
                     valname = '%s.%s' % (name, valname)
                     self._find(tests, val, valname, module, source_lines,
-                               globs, ignore_imports, seen)
+                               globs, seen)
 
     def _get_test(self, obj, name, module, globs, source_lines):
         """
@@ -1015,7 +1071,10 @@
             filename = None
         else:
             filename = getattr(module, '__file__', module.__name__)
-        return self._doctest_factory(docstring, globs, name, filename, lineno)
+            if filename[-4:] in (".pyc", ".pyo"):
+                filename = filename[:-1]
+        return self._parser.get_doctest(docstring, globs, name,
+                                        filename, lineno)
 
     def _find_lineno(self, obj, source_lines):
         """
@@ -1066,12 +1125,9 @@
         return None
 
 ######################################################################
-## 4. DocTest Runner
+## 5. DocTest Runner
 ######################################################################
 
-# [XX] Should overridable methods (eg DocTestRunner.check_output) be
-# named with a leading underscore?
-
 class DocTestRunner:
     """
     A class used to run DocTest test cases, and accumulate statistics.
@@ -1112,12 +1168,11 @@
         0
 
     The comparison between expected outputs and actual outputs is done
-    by the `check_output` method.  This comparison may be customized
-    with a number of option flags; see the documentation for `testmod`
-    for more information.  If the option flags are insufficient, then
-    the comparison may also be customized by subclassing
-    DocTestRunner, and overriding the methods `check_output` and
-    `output_difference`.
+    by an `OutputChecker`.  This comparison may be customized with a
+    number of option flags; see the documentation for `testmod` for
+    more information.  If the option flags are insufficient, then the
+    comparison may also be customized by passing a subclass of
+    `OutputChecker` to the constructor.
 
     The test runner's display output can be controlled in two ways.
     First, an output function (`out) can be passed to
@@ -1132,10 +1187,14 @@
     # separate sections of the summary.
     DIVIDER = "*" * 70
 
-    def __init__(self, verbose=None, optionflags=0):
+    def __init__(self, checker=None, verbose=None, optionflags=0):
         """
         Create a new test runner.
 
+        Optional keyword arg `checker` is the `OutputChecker` that
+        should be used to compare the expected outputs and actual
+        outputs of doctest examples.
+
         Optional keyword arg 'verbose' prints lots of stuff if true,
         only failures if false; by default, it's true iff '-v' is in
         sys.argv.
@@ -1145,10 +1204,12 @@
         it displays failures.  See the documentation for `testmod` for
         more information.
         """
+        self._checker = checker or OutputChecker()
         if verbose is None:
             verbose = '-v' in sys.argv
         self._verbose = verbose
         self.optionflags = optionflags
+        self.original_optionflags = optionflags
 
         # Keep track of the examples we've run.
         self.tries = 0
@@ -1159,114 +1220,6 @@
         self._fakeout = _SpoofOut()
 
     #/////////////////////////////////////////////////////////////////
-    # Output verification methods
-    #/////////////////////////////////////////////////////////////////
-    # These two methods should be updated together, since the
-    # output_difference method needs to know what should be considered
-    # to match by check_output.
-
-    def check_output(self, want, got):
-        """
-        Return True iff the actual output (`got`) matches the expected
-        output (`want`).  These strings are always considered to match
-        if they are identical; but depending on what option flags the
-        test runner is using, several non-exact match types are also
-        possible.  See the documentation for `TestRunner` for more
-        information about option flags.
-        """
-        # Handle the common case first, for efficiency:
-        # if they're string-identical, always return true.
-        if got == want:
-            return True
-
-        # The values True and False replaced 1 and 0 as the return
-        # value for boolean comparisons in Python 2.3.
-        if not (self.optionflags & DONT_ACCEPT_TRUE_FOR_1):
-            if (got,want) == ("True\n", "1\n"):
-                return True
-            if (got,want) == ("False\n", "0\n"):
-                return True
-
-        # <BLANKLINE> can be used as a special sequence to signify a
-        # blank line, unless the DONT_ACCEPT_BLANKLINE flag is used.
-        if not (self.optionflags & DONT_ACCEPT_BLANKLINE):
-            # Replace <BLANKLINE> in want with a blank line.
-            want = re.sub('(?m)^%s\s*?$' % re.escape(BLANKLINE_MARKER),
-                          '', want)
-            # If a line in got contains only spaces, then remove the
-            # spaces.
-            got = re.sub('(?m)^\s*?$', '', got)
-            if got == want:
-                return True
-
-        # This flag causes doctest to ignore any differences in the
-        # contents of whitespace strings.  Note that this can be used
-        # in conjunction with the ELLISPIS flag.
-        if (self.optionflags & NORMALIZE_WHITESPACE):
-            got = ' '.join(got.split())
-            want = ' '.join(want.split())
-            if got == want:
-                return True
-
-        # The ELLIPSIS flag says to let the sequence "..." in `want`
-        # match any substring in `got`.  We implement this by
-        # transforming `want` into a regular expression.
-        if (self.optionflags & ELLIPSIS):
-            # Escape any special regexp characters
-            want_re = re.escape(want)
-            # Replace ellipsis markers ('...') with .*
-            want_re = want_re.replace(re.escape(ELLIPSIS_MARKER), '.*')
-            # Require that it matches the entire string; and set the
-            # re.DOTALL flag (with '(?s)').
-            want_re = '(?s)^%s$' % want_re
-            # Check if the `want_re` regexp matches got.
-            if re.match(want_re, got):
-                return True
-
-        # We didn't find any match; return false.
-        return False
-
-    def output_difference(self, want, got):
-        """
-        Return a string describing the differences between the
-        expected output (`want`) and the actual output (`got`).
-        """
-        # If <BLANKLINE>s are being used, then replace <BLANKLINE>
-        # with blank lines in the expected output string.
-        if not (self.optionflags & DONT_ACCEPT_BLANKLINE):
-            want = re.sub('(?m)^%s$' % re.escape(BLANKLINE_MARKER), '', want)
-
-        # Check if we should use diff.  Don't use diff if the actual
-        # or expected outputs are too short, or if the expected output
-        # contains an ellipsis marker.
-        if ((self.optionflags & (UNIFIED_DIFF | CONTEXT_DIFF)) and
-            want.count('\n') > 2 and got.count('\n') > 2 and
-            not (self.optionflags & ELLIPSIS and '...' in want)):
-            # Split want & got into lines.
-            want_lines = [l+'\n' for l in want.split('\n')]
-            got_lines = [l+'\n' for l in got.split('\n')]
-            # Use difflib to find their differences.
-            if self.optionflags & UNIFIED_DIFF:
-                diff = difflib.unified_diff(want_lines, got_lines, n=2,
-                                            fromfile='Expected', tofile='Got')
-                kind = 'unified'
-            elif self.optionflags & CONTEXT_DIFF:
-                diff = difflib.context_diff(want_lines, got_lines, n=2,
-                                            fromfile='Expected', tofile='Got')
-                kind = 'context'
-            else:
-                assert 0, 'Bad diff option'
-            # Remove trailing whitespace on diff output.
-            diff = [line.rstrip() + '\n' for line in diff]
-            return _tag_msg("Differences (" + kind + " diff)",
-                            ''.join(diff))
-
-        # If we're not using diff, then simply list the expected
-        # output followed by the actual output.
-        return (_tag_msg("Expected", want or "Nothing") +
-                _tag_msg("Got", got))
-
-    #/////////////////////////////////////////////////////////////////
     # Reporting methods
     #/////////////////////////////////////////////////////////////////
 
@@ -1292,80 +1245,62 @@
         Report that the given example failed.
         """
         # Print an error message.
-        out(self.__failure_header(test, example) +
-            self.output_difference(example.want, got))
+        out(self._failure_header(test, example) +
+            self._checker.output_difference(example.want, got,
+                                            self.optionflags))
 
     def report_unexpected_exception(self, out, test, example, exc_info):
         """
         Report that the given example raised an unexpected exception.
         """
-        # Get a traceback message.
-        excout = StringIO()
-        exc_type, exc_val, exc_tb = exc_info
-        traceback.print_exception(exc_type, exc_val, exc_tb, file=excout)
-        exception_tb = excout.getvalue()
-        # Print an error message.
-        out(self.__failure_header(test, example) +
-            _tag_msg("Exception raised", exception_tb))
+        out(self._failure_header(test, example) +
+            _tag_msg("Exception raised", _exception_traceback(exc_info)))
 
-    def __failure_header(self, test, example):
-        s = (self.DIVIDER + "\n" +
-             _tag_msg("Failure in example", example.source))
-        if test.filename is None:
-            # [XX] I'm not putting +1 here, to give the same output
-            # as the old version.  But I think it *should* go here.
-            return s + ("from line #%s of %s\n" %
-                        (example.lineno, test.name))
-        elif test.lineno is None:
-            return s + ("from line #%s of %s in %s\n" %
-                        (example.lineno+1, test.name, test.filename))
+    def _failure_header(self, test, example):
+        out = [self.DIVIDER]
+        if test.filename:
+            if test.lineno is not None and example.lineno is not None:
+                lineno = test.lineno + example.lineno + 1
+            else:
+                lineno = '?'
+            out.append('File "%s", line %s, in %s' %
+                       (test.filename, lineno, test.name))
         else:
-            lineno = test.lineno+example.lineno+1
-            return s + ("from line #%s of %s (%s)\n" %
-                        (lineno, test.filename, test.name))
+            out.append('Line %s, in %s' % (example.lineno+1, test.name))
+        out.append('Failed example:')
+        source = example.source
+        if source.endswith('\n'):
+            source = source[:-1]
+        out.append('    ' + '\n    '.join(source.split('\n')))
+        return '\n'.join(out)+'\n'
 
     #/////////////////////////////////////////////////////////////////
     # DocTest Running
     #/////////////////////////////////////////////////////////////////
 
     # A regular expression for handling `want` strings that contain
-    # expected exceptions.  It divides `want` into two pieces: the
-    # pre-exception output (`out`) and the exception message (`exc`),
-    # as generated by traceback.format_exception_only().  (I assume
-    # that the exception_only message is the first non-indented line
-    # starting with word characters after the "Traceback ...".)
-    _EXCEPTION_RE = re.compile(('^(?P<out>.*)'
-                                '^(?P<hdr>Traceback \((?:%s|%s)\):)\s*$.*?'
-                                '^(?P<exc>\w+.*)') %
-                               ('most recent call last', 'innermost last'),
-                               re.MULTILINE | re.DOTALL)
+    # expected exceptions.  It divides `want` into three pieces:
+    #    - the pre-exception output (`want`)
+    #    - the traceback header line (`hdr`)
+    #    - the exception message (`msg`), as generated by
+    #      traceback.format_exception_only()
+    # `msg` may have multiple lines.  We assume/require that the
+    # exception message is the first non-indented line starting with a word
+    # character following the traceback header line.
+    _EXCEPTION_RE = re.compile(r"""
+        (?P<want> .*?)   # suck up everything until traceback header
+        # Grab the traceback header.  Different versions of Python have
+        # said different things on the first traceback line.
+        ^(?P<hdr> Traceback\ \(
+            (?: most\ recent\ call\ last
+            |   innermost\ last
+            ) \) :
+        )
+        \s* $  # toss trailing whitespace on traceback header
+        .*?    # don't blink:  absorb stuff until a line *starts* with \w
+        ^ (?P<msg> \w+ .*)
+        """, re.VERBOSE | re.MULTILINE | re.DOTALL)
 
-    _OPTION_DIRECTIVE_RE = re.compile('\s*doctest:\s*(?P<flags>[^#\n]*)')
-
-    def __handle_directive(self, example):
-        """
-        Check if the given example is actually a directive to doctest
-        (to turn an optionflag on or off); and if it is, then handle
-        the directive.
-
-        Return true iff the example is actually a directive (and so
-        should not be executed).
-
-        """
-        m = self._OPTION_DIRECTIVE_RE.match(example.source)
-        if m is None:
-            return False
-
-        for flag in m.group('flags').upper().split():
-            if (flag[:1] not in '+-' or
-                flag[1:] not in OPTIONFLAGS_BY_NAME):
-                raise ValueError('Bad doctest option directive: '+flag)
-            if flag[0] == '+':
-                self.optionflags |= OPTIONFLAGS_BY_NAME[flag[1:]]
-            else:
-                self.optionflags &= ~OPTIONFLAGS_BY_NAME[flag[1:]]
-        return True
-
     def __run(self, test, compileflags, out):
         """
         Run the examples in `test`.  Write the outcome of each example
@@ -1385,10 +1320,14 @@
 
         # Process each example.
         for example in test.examples:
-            # Check if it's an option directive.  If it is, then handle
-            # it, and go on to the next example.
-            if self.__handle_directive(example):
-                continue
+            # Merge in the example's options.
+            self.optionflags = original_optionflags
+            if example.options:
+                for (optionflag, val) in example.options.items():
+                    if val:
+                        self.optionflags |= optionflag
+                    else:
+                        self.optionflags &= ~optionflag
 
             # Record that we started this example.
             tries += 1
@@ -1398,11 +1337,8 @@
             # any exception that gets raised.  (But don't intercept
             # keyboard interrupts.)
             try:
-                # If the example is a compound statement on one line,
-                # like "if 1: print 2", then compile() requires a
-                # trailing newline.  Rather than analyze that, always
-                # append one (it never hurts).
-                exec compile(example.source + '\n', "<string>", "single",
+                # Don't blink!  This is where the user's code gets run.
+                exec compile(example.source, "<string>", "single",
                              compileflags, 1) in test.globs
                 exception = None
             except KeyboardInterrupt:
@@ -1410,16 +1346,14 @@
             except:
                 exception = sys.exc_info()
 
-            # Extract the example's actual output from fakeout, and
-            # write it to `got`.  Add a terminating newline if it
-            # doesn't have already one.
-            got = self._fakeout.getvalue()
+            got = self._fakeout.getvalue()  # the actual output
             self._fakeout.truncate(0)
 
             # If the example executed without raising any exceptions,
             # then verify its output and report its outcome.
             if exception is None:
-                if self.check_output(example.want, got):
+                if self._checker.check_output(example.want, got,
+                                              self.optionflags):
                     self.report_success(out, test, example, got)
                 else:
                     self.report_failure(out, test, example, got)
@@ -1439,18 +1373,19 @@
                                                      exc_info)
                     failures += 1
                 else:
-                    exc_hdr = m.group('hdr')+'\n' # Exception header
+                    e_want, e_msg = m.group('want', 'msg')
                     # The test passes iff the pre-exception output and
                     # the exception description match the values given
                     # in `want`.
-                    if (self.check_output(m.group('out'), got) and
-                        self.check_output(m.group('exc'), exc_msg)):
-                        # Is +exc_msg the right thing here??
+                    if (self._checker.check_output(e_want, got,
+                                                   self.optionflags) and
+                        self._checker.check_output(e_msg, exc_msg,
+                                                   self.optionflags)):
                         self.report_success(out, test, example,
-                                            got+exc_hdr+exc_msg)
+                                       got + _exception_traceback(exc_info))
                     else:
                         self.report_failure(out, test, example,
-                                            got+exc_hdr+exc_msg)
+                                       got + _exception_traceback(exc_info))
                         failures += 1
 
         # Restore the option flags (in case they were modified)
@@ -1492,15 +1427,31 @@
         """
         if compileflags is None:
             compileflags = _extract_future_flags(test.globs)
+
+        save_stdout = sys.stdout
         if out is None:
-            out = sys.stdout.write
-        saveout = sys.stdout
+            out = save_stdout.write
+        sys.stdout = self._fakeout
 
+        # Patch pdb.set_trace to restore sys.stdout, so that interactive
+        # debugging output is visible (not still redirected to self._fakeout).
+        # Note that we run "the real" pdb.set_trace (captured at doctest
+        # import time) in our replacement.  Because the current run() may
+        # run another doctest (and so on), the current pdb.set_trace may be
+        # our set_trace function, which changes sys.stdout.  If we called
+        # a chain of those, we wouldn't be left with the save_stdout
+        # *this* run() invocation wants.
+        def set_trace():
+            sys.stdout = save_stdout
+            real_pdb_set_trace()
+
+        save_set_trace = pdb.set_trace
+        pdb.set_trace = set_trace
         try:
-            sys.stdout = self._fakeout
             return self.__run(test, compileflags, out)
         finally:
-            sys.stdout = saveout
+            sys.stdout = save_stdout
+            pdb.set_trace = save_set_trace
             if clear_globs:
                 test.globs.clear()
 
@@ -1561,6 +1512,116 @@
             print "Test passed."
         return totalf, totalt
 
+class OutputChecker:
+    """
+    A class used to check the whether the actual output from a doctest
+    example matches the expected output.  `OutputChecker` defines two
+    methods: `check_output`, which compares a given pair of outputs,
+    and returns true if they match; and `output_difference`, which
+    returns a string describing the differences between two outputs.
+    """
+    def check_output(self, want, got, optionflags):
+        """
+        Return True iff the actual output from an example (`got`)
+        matches the expected output (`want`).  These strings are
+        always considered to match if they are identical; but
+        depending on what option flags the test runner is using,
+        several non-exact match types are also possible.  See the
+        documentation for `TestRunner` for more information about
+        option flags.
+        """
+        # Handle the common case first, for efficiency:
+        # if they're string-identical, always return true.
+        if got == want:
+            return True
+
+        # The values True and False replaced 1 and 0 as the return
+        # value for boolean comparisons in Python 2.3.
+        if not (optionflags & DONT_ACCEPT_TRUE_FOR_1):
+            if (got,want) == ("True\n", "1\n"):
+                return True
+            if (got,want) == ("False\n", "0\n"):
+                return True
+
+        # <BLANKLINE> can be used as a special sequence to signify a
+        # blank line, unless the DONT_ACCEPT_BLANKLINE flag is used.
+        if not (optionflags & DONT_ACCEPT_BLANKLINE):
+            # Replace <BLANKLINE> in want with a blank line.
+            want = re.sub('(?m)^%s\s*?$' % re.escape(BLANKLINE_MARKER),
+                          '', want)
+            # If a line in got contains only spaces, then remove the
+            # spaces.
+            got = re.sub('(?m)^\s*?$', '', got)
+            if got == want:
+                return True
+
+        # This flag causes doctest to ignore any differences in the
+        # contents of whitespace strings.  Note that this can be used
+        # in conjunction with the ELLISPIS flag.
+        if optionflags & NORMALIZE_WHITESPACE:
+            got = ' '.join(got.split())
+            want = ' '.join(want.split())
+            if got == want:
+                return True
+
+        # The ELLIPSIS flag says to let the sequence "..." in `want`
+        # match any substring in `got`.
+        if optionflags & ELLIPSIS:
+            if _ellipsis_match(want, got):
+                return True
+
+        # We didn't find any match; return false.
+        return False
+
+    def output_difference(self, want, got, optionflags):
+        """
+        Return a string describing the differences between the
+        expected output for an example (`want`) and the actual output
+        (`got`).  `optionflags` is the set of option flags used to
+        compare `want` and `got`.  `indent` is the indentation of the
+        original example.
+        """
+        
+        # If <BLANKLINE>s are being used, then replace blank lines
+        # with <BLANKLINE> in the actual output string.
+        if not (optionflags & DONT_ACCEPT_BLANKLINE):
+            got = re.sub('(?m)^[ ]*(?=\n)', BLANKLINE_MARKER, got)
+
+        # Check if we should use diff.  Don't use diff if the actual
+        # or expected outputs are too short, or if the expected output
+        # contains an ellipsis marker.
+        if ((optionflags & (UNIFIED_DIFF | CONTEXT_DIFF)) and
+            want.count('\n') > 2 and got.count('\n') > 2 and
+            not (optionflags & ELLIPSIS and '...' in want)):
+            # Split want & got into lines.
+            want_lines = [l+'\n' for l in want.split('\n')]
+            got_lines = [l+'\n' for l in got.split('\n')]
+            # Use difflib to find their differences.
+            if optionflags & UNIFIED_DIFF:
+                diff = difflib.unified_diff(want_lines, got_lines, n=2,
+                                            fromfile='Expected', tofile='Got')
+                kind = 'unified'
+            elif optionflags & CONTEXT_DIFF:
+                diff = difflib.context_diff(want_lines, got_lines, n=2,
+                                            fromfile='Expected', tofile='Got')
+                kind = 'context'
+            else:
+                assert 0, 'Bad diff option'
+            # Remove trailing whitespace on diff output.
+            diff = [line.rstrip() + '\n' for line in diff]
+            return _tag_msg("Differences (" + kind + " diff)",
+                            ''.join(diff))
+
+        # If we're not using diff, then simply list the expected
+        # output followed by the actual output.
+        if want.endswith('\n'):
+            want = want[:-1]
+        want = '    ' + '\n    '.join(want.split('\n'))
+        if got.endswith('\n'):
+            got = got[:-1]
+        got = '    ' + '\n    '.join(got.split('\n'))
+        return "Expected:\n%s\nGot:\n%s\n" % (want, got)
+
 class DocTestFailure(Exception):
     """A DocTest example has failed in debugging mode.
 
@@ -1598,7 +1659,7 @@
 
     def __str__(self):
         return str(self.test)
-    
+
 class DebugRunner(DocTestRunner):
     r"""Run doc tests but raise an exception as soon as there is a failure.
 
@@ -1606,7 +1667,8 @@
        It contains the test, the example, and the original exception:
 
          >>> runner = DebugRunner(verbose=False)
-         >>> test = DocTest('>>> raise KeyError\n42', {}, 'foo', 'foo.py', 0)
+         >>> test = DocTestParser().get_doctest('>>> raise KeyError\n42',
+         ...                                    {}, 'foo', 'foo.py', 0)
          >>> try:
          ...     runner.run(test)
          ... except UnexpectedException, failure:
@@ -1629,7 +1691,7 @@
 
        If the output doesn't match, then a DocTestFailure is raised:
 
-         >>> test = DocTest('''
+         >>> test = DocTestParser().get_doctest('''
          ...      >>> x = 1
          ...      >>> x
          ...      2
@@ -1661,7 +1723,7 @@
          >>> test.globs
          {'x': 1}
 
-         >>> test = DocTest('''
+         >>> test = DocTestParser().get_doctest('''
          ...      >>> x = 2
          ...      >>> raise KeyError
          ...      ''', {}, 'foo', 'foo.py', 0)
@@ -1670,14 +1732,14 @@
          Traceback (most recent call last):
          ...
          UnexpectedException: <DocTest foo from foo.py:0 (2 examples)>
-         
+
          >>> del test.globs['__builtins__']
          >>> test.globs
          {'x': 2}
 
        But the globals are cleared if there is no error:
 
-         >>> test = DocTest('''
+         >>> test = DocTestParser().get_doctest('''
          ...      >>> x = 2
          ...      ''', {}, 'foo', 'foo.py', 0)
 
@@ -1702,7 +1764,7 @@
         raise DocTestFailure(test, example, got)
 
 ######################################################################
-## 5. Test Functions
+## 6. Test Functions
 ######################################################################
 # These should be backwards compatible.
 
@@ -1718,7 +1780,7 @@
     are not skipped.
 
     Also test examples reachable from dict m.__test__ if it exists and is
-    not None.  m.__dict__ maps names to functions, classes and strings;
+    not None.  m.__test__ maps names to functions, classes and strings;
     function and class docstrings are tested even if the name is private;
     strings are tested directly, as if they were docstrings.
 
@@ -1741,12 +1803,6 @@
     Optional keyword arg "verbose" prints lots of stuff if true, prints
     only failures if false; by default, it's true iff "-v" is in sys.argv.
 
-    Optional keyword arg "isprivate" specifies a function used to
-    determine whether a name is private.  The default function is
-    treat all functions as public.  Optionally, "isprivate" can be
-    set to doctest.is_private to skip over functions marked as private
-    using the underscore naming convention; see its docs for details.
-
     Optional keyword arg "report" prints a summary at the end when true,
     else prints nothing at the end.  In verbose mode, the summary is
     detailed, else very brief (in fact, empty if all tests passed).
@@ -1793,6 +1849,12 @@
     first unexpected exception or failure. This allows failures to be
     post-mortem debugged.
 
+    Deprecated in Python 2.4:
+    Optional keyword arg "isprivate" specifies a function used to
+    determine whether a name is private.  The default function is
+    treat all functions as public.  Optionally, "isprivate" can be
+    set to doctest.is_private to skip over functions marked as private
+    using the underscore naming convention; see its docs for details.
     """
 
     """ [XX] This is no longer true:
@@ -1804,6 +1866,11 @@
     displaying a summary.  Invoke doctest.master.summarize(verbose)
     when you're done fiddling.
     """
+    if isprivate is not None:
+        warnings.warn("the isprivate argument is deprecated; "
+                      "examine DocTestFinder.find() lists instead",
+                      DeprecationWarning)
+
     # If no module was given, then use __main__.
     if m is None:
         # DWA - m will still be None if this wasn't invoked from the command
@@ -1820,7 +1887,7 @@
         name = m.__name__
 
     # Find, parse, and run all tests in the given module.
-    finder = DocTestFinder(namefilter=isprivate)
+    finder = DocTestFinder(_namefilter=isprivate)
 
     if raise_on_error:
         runner = DebugRunner(verbose=verbose, optionflags=optionflags)
@@ -1859,7 +1926,7 @@
         runner.run(test, compileflags=compileflags)
 
 ######################################################################
-## 6. Tester
+## 7. Tester
 ######################################################################
 # This is provided only for backwards compatibility.  It's not
 # actually used in any way.
@@ -1867,6 +1934,10 @@
 class Tester:
     def __init__(self, mod=None, globs=None, verbose=None,
                  isprivate=None, optionflags=0):
+
+        warnings.warn("class Tester is deprecated; "
+                      "use class doctest.DocTestRunner instead",
+                      DeprecationWarning, stacklevel=2)
         if mod is None and globs is None:
             raise TypeError("Tester.__init__: must specify mod or globs")
         if mod is not None and not _ismodule(mod):
@@ -1879,12 +1950,12 @@
         self.verbose = verbose
         self.isprivate = isprivate
         self.optionflags = optionflags
-        self.testfinder = DocTestFinder(namefilter=isprivate)
+        self.testfinder = DocTestFinder(_namefilter=isprivate)
         self.testrunner = DocTestRunner(verbose=verbose,
                                         optionflags=optionflags)
 
     def runstring(self, s, name):
-        test = DocTest(s, self.globs, name, None, None)
+        test = DocTestParser().get_doctest(s, self.globs, name, None, None)
         if self.verbose:
             print "Running string", name
         (f,t) = self.testrunner.run(test)
@@ -1892,11 +1963,10 @@
             print f, "of", t, "examples failed in string", name
         return (f,t)
 
-    def rundoc(self, object, name=None, module=None, ignore_imports=True):
+    def rundoc(self, object, name=None, module=None):
         f = t = 0
         tests = self.testfinder.find(object, name, module=module,
-                                     globs=self.globs,
-                                     ignore_imports=ignore_imports)
+                                     globs=self.globs)
         for test in tests:
             (f2, t2) = self.testrunner.run(test)
             (f,t) = (f+f2, t+t2)
@@ -1906,8 +1976,9 @@
         import new
         m = new.module(name)
         m.__dict__.update(d)
-        ignore_imports = (module is not None)
-        return self.rundoc(m, name, module, ignore_imports)
+        if module is None:
+            module = False
+        return self.rundoc(m, name, module)
 
     def run__test__(self, d, name):
         import new
@@ -1930,14 +2001,17 @@
             d[name] = f, t
 
 ######################################################################
-## 7. Unittest Support
+## 8. Unittest Support
 ######################################################################
 
 class DocTestCase(unittest.TestCase):
 
-    def __init__(self, test, optionflags=0, setUp=None, tearDown=None):
+    def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
+                 checker=None):
+
         unittest.TestCase.__init__(self)
         self._dt_optionflags = optionflags
+        self._dt_checker = checker
         self._dt_test = test
         self._dt_setUp = setUp
         self._dt_tearDown = tearDown
@@ -1954,7 +2028,8 @@
         test = self._dt_test
         old = sys.stdout
         new = StringIO()
-        runner = DocTestRunner(optionflags=self._dt_optionflags, verbose=False)
+        runner = DocTestRunner(optionflags=self._dt_optionflags,
+                               checker=self._dt_checker, verbose=False)
 
         try:
             runner.DIVIDER = "-"*70
@@ -1970,7 +2045,7 @@
         if test.lineno is None:
             lineno = 'unknown line number'
         else:
-            lineno = 'line %s' % test.lineno
+            lineno = '%s' % test.lineno
         lname = '.'.join(test.name.split('.')[-1:])
         return ('Failed doctest test for %s\n'
                 '  File "%s", line %s, in %s\n\n%s'
@@ -1989,7 +2064,7 @@
            UnexpectedException errors if there is an unexepcted
            exception:
 
-             >>> test = DocTest('>>> raise KeyError\n42',
+             >>> test = DocTestParser().get_doctest('>>> raise KeyError\n42',
              ...                {}, 'foo', 'foo.py', 0)
              >>> case = DocTestCase(test)
              >>> try:
@@ -2014,7 +2089,7 @@
 
            If the output doesn't match, then a DocTestFailure is raised:
 
-             >>> test = DocTest('''
+             >>> test = DocTestParser().get_doctest('''
              ...      >>> x = 1
              ...      >>> x
              ...      2
@@ -2043,8 +2118,9 @@
 
            """
 
-        runner = DebugRunner(verbose = False, optionflags=self._dt_optionflags)
-        runner.run(self._dt_test, out=nooutput)
+        runner = DebugRunner(optionflags=self._dt_optionflags,
+                             checker=self._dt_checker, verbose=False)
+        runner.run(self._dt_test)
 
     def id(self):
         return self._dt_test.name
@@ -2058,12 +2134,10 @@
     def shortDescription(self):
         return "Doctest: " + self._dt_test.name
 
-def nooutput(*args):
-    pass
-
 def DocTestSuite(module=None, globs=None, extraglobs=None,
                  optionflags=0, test_finder=None,
-                 setUp=lambda: None, tearDown=lambda: None):
+                 setUp=lambda: None, tearDown=lambda: None,
+                 checker=None):
     """
     Convert doctest tests for a mudule to a unittest test suite.
 
@@ -2096,12 +2170,11 @@
             continue
         if not test.filename:
             filename = module.__file__
-            if filename.endswith(".pyc"):
+            if filename[-4:] in (".pyc", ".pyo"):
                 filename = filename[:-1]
-            elif filename.endswith(".pyo"):
-                filename = filename[:-1]
             test.filename = filename
-        suite.addTest(DocTestCase(test, optionflags, setUp, tearDown))
+        suite.addTest(DocTestCase(test, optionflags, setUp, tearDown,
+                                  checker))
 
     return suite
 
@@ -2131,7 +2204,7 @@
     if globs is None:
         globs = {}
 
-    test = DocTest(doc, globs, name, path, 0)
+    test = DocTestParser().get_doctest(doc, globs, name, path, 0)
 
     return DocFileCase(test, optionflags, setUp, tearDown)
 
@@ -2175,7 +2248,7 @@
     return suite
 
 ######################################################################
-## 8. Debugging Support
+## 9. Debugging Support
 ######################################################################
 
 def script_from_examples(s):
@@ -2211,33 +2284,33 @@
        ...           '''
 
        >>> print script_from_examples(text)
-       #        Here are examples of simple math.
+       # Here are examples of simple math.
        #
-       #            Python has super accurate integer addition
+       #     Python has super accurate integer addition
        #
        2 + 2
        # Expected:
-       #     5
+       ## 5
        #
-       #            And very friendly error messages:
+       #     And very friendly error messages:
        #
        1/0
        # Expected:
-       #     To Infinity
-       #     And
-       #     Beyond
+       ## To Infinity
+       ## And
+       ## Beyond
        #
-       #            You can use logic if you want:
+       #     You can use logic if you want:
        #
        if 0:
           blah
           blah
        <BLANKLINE>
        #
-       #            Ho hum
+       #     Ho hum
        """
 
-    return Parser('<string>', s).get_program()
+    return DocTestParser().get_program(s)
 
 def _want_comment(example):
     """
@@ -2310,7 +2383,7 @@
     debug_script(testsrc, pm, module.__dict__)
 
 ######################################################################
-## 9. Example Usage
+## 10. Example Usage
 ######################################################################
 class _TestClass:
     """
@@ -2404,6 +2477,8 @@
 #            }
 
 def test1(): r"""
+>>> warnings.filterwarnings("ignore", "class Tester", DeprecationWarning,
+...                         "doctest", 0)
 >>> from doctest import Tester
 >>> t = Tester(globs={'x': 42}, verbose=0)
 >>> t.runstring(r'''
@@ -2412,10 +2487,13 @@
 ...      42
 ... ''', 'XYZ')
 **********************************************************************
-Failure in example: print x
-from line #2 of XYZ
-Expected: 42
-Got: 84
+Line 3, in XYZ
+Failed example:
+    print x
+Expected:
+    42
+Got:
+    84
 (1, 2)
 >>> t.runstring(">>> x = x * 2\n>>> print x\n84\n", 'example2')
 (0, 2)
@@ -2438,6 +2516,8 @@
 """
 
 def test2(): r"""
+        >>> warnings.filterwarnings("ignore", "class Tester",
+        ...                         DeprecationWarning, "doctest", 0)
         >>> t = Tester(globs={}, verbose=1)
         >>> test = r'''
         ...    # just an example
@@ -2457,6 +2537,8 @@
         (0, 2)
 """
 def test3(): r"""
+        >>> warnings.filterwarnings("ignore", "class Tester",
+        ...                         DeprecationWarning, "doctest", 0)
         >>> t = Tester(globs={}, verbose=0)
         >>> def _f():
         ...     '''Trivial docstring example.
@@ -2491,17 +2573,13 @@
 
         Tests that objects outside m1 are excluded:
 
-        >>> t = Tester(globs={}, verbose=0, isprivate=is_private)
-        >>> t.rundict(m1.__dict__, "rundict_test", m1)  # _f, f2 and g2 and h2 skipped
-        (0, 3)
-
-        Again, but with the default isprivate function allowing _f:
-
+        >>> warnings.filterwarnings("ignore", "class Tester",
+        ...                         DeprecationWarning, "doctest", 0)
         >>> t = Tester(globs={}, verbose=0)
-        >>> t.rundict(m1.__dict__, "rundict_test_pvt", m1)  # Only f2, g2 and h2 skipped
+        >>> t.rundict(m1.__dict__, "rundict_test", m1)  # f2 and g2 and h2 skipped
         (0, 4)
 
-        And once more, not excluding stuff outside m1:
+        Once more, not excluding stuff outside m1:
 
         >>> t = Tester(globs={}, verbose=0)
         >>> t.rundict(m1.__dict__, "rundict_test_pvt")  # None are skipped.
@@ -2510,8 +2588,8 @@
         The exclusion of objects from outside the designated module is
         meant to be invoked automagically by testmod.
 
-        >>> testmod(m1, isprivate=is_private, verbose=False)
-        (0, 3)
+        >>> testmod(m1, verbose=False)
+        (0, 4)
 """
 
 def _test():



More information about the Zope3-Checkins mailing list