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

Jim Fulton jim at zope.com
Sat Aug 28 15:50:47 EDT 2004


Log message for revision 27324:
  Merged from trunk:
  
    r27323 | jim | 2004-08-28 15:31:22 -0400 (Sat, 28 Aug 2004) | 15 lines
  
  Integrated the latest doctest rom the Python cvs.
  
  This brought two backward-incompatible changes:
  
  - setUp and tearDown functions are now passed a test 
    argument, which is a doctest.DocTest.  This provides access to the
    test globals.
  
  - The names of doctest reporting options for requesting diff output
    have changed.
  
  Thesechanges are both positive for the long run, despite the
  short-term backward-incompatability. Better before X3.0 final than
  later. 
  


Changed:
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/__init__.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/tests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/ifacemodule/tests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/servicemodule/tests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/tests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/utilitymodule/tests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/viewmodule/tests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/zcmlmodule/tests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/form/browser/tests/test_registrations.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/onlinehelp/tests/test_onlinehelp.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/browser/tests/test_addMenuItem.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/xmlrpc/ftests.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/securitypolicy/tests/test_zopepolicy.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/tests/functional.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/app/tests/placelesssetup.py
  U   Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py


-=-
Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/__init__.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/__init__.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/__init__.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -184,7 +184,7 @@
       >>> names = module['tests'].keys()
       >>> names.sort()
       >>> names
-      ['Root', 'pprint', 'rootLocation', 'setUp', 'tearDown', 'test_suite']
+      ['Root', 'pprint', 'rootLocation', 'setUp', 'test_suite']
     """
     implements(ILocation, IModuleDocumentation)
 

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/tests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/tests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/classmodule/tests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -44,7 +44,7 @@
 from zope.app.apidoc.interfaces import IDocumentationModule
 
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
     module = ClassModule()
     module.__name__ = ''
@@ -74,10 +74,6 @@
                       ReStructuredTextToHTMLRenderer)
 
 
-def tearDown():
-    placelesssetup.tearDown()
-
-
 def foo(cls, bar=1, *args):
     """This is the foo function."""
 foo.deprecated = True
@@ -110,7 +106,7 @@
 def test_suite():
     return unittest.TestSuite((
         DocTestSuite('zope.app.apidoc.classmodule.browser',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         DocTestSuite('zope.app.apidoc.classmodule'),
         ))
 

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/ifacemodule/tests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/ifacemodule/tests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/ifacemodule/tests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -92,7 +92,7 @@
     return view
     
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
     provideInterface(None, IDocumentationModule)
     provideInterface('IInterfaceModule', IInterfaceModule)
@@ -121,17 +121,15 @@
     sm.defineService('Foo', IFoo)
     sm.provideService('Foo', Foo())
 
-def tearDown():
-    placelesssetup.tearDown()
     
 def test_suite():
     return unittest.TestSuite((
         DocTestSuite('zope.app.apidoc.ifacemodule',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         DocTestSuite('zope.app.apidoc.ifacemodule.menu',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         DocTestSuite('zope.app.apidoc.ifacemodule.browser',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         ))
 
 if __name__ == '__main__':

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/servicemodule/tests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/servicemodule/tests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/servicemodule/tests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -25,21 +25,17 @@
 from zope.app.traversing.interfaces import IPhysicallyLocatable
 from zope.app.location.traversing import LocationPhysicallyLocatable
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
     ztapi.provideAdapter(None, IUniqueId, LocationUniqueId)
     ztapi.provideAdapter(None, IPhysicallyLocatable,
                          LocationPhysicallyLocatable)
 
-def tearDown():
-    placelesssetup.tearDown()
-
-
 def test_suite():
     return unittest.TestSuite((
         DocTestSuite('zope.app.apidoc.servicemodule'),
         DocTestSuite('zope.app.apidoc.servicemodule.browser',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         ))
 
 if __name__ == '__main__':

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/tests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/tests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/tests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -33,7 +33,7 @@
 from zope.app.renderer.rest import ReStructuredTextToHTMLRenderer
 
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
     ztapi.provideUtility(IDocumentationModule, InterfaceModule(),
                            'Interface')
@@ -45,10 +45,7 @@
     ztapi.browserView(IReStructuredTextSource, '', 
                       ReStructuredTextToHTMLRenderer)
 
-def tearDown():
-    placelesssetup.tearDown()
 
-
 # Generally useful classes and functions
 
 class Root:
@@ -108,9 +105,9 @@
 def test_suite():
     return unittest.TestSuite((
         DocTestSuite('zope.app.apidoc',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         DocTestSuite('zope.app.apidoc.browser.apidoc',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         DocTestSuite('zope.app.apidoc.utilities'),
         DocTestSuite('zope.app.apidoc.tests'),
         ))

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/utilitymodule/tests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/utilitymodule/tests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/utilitymodule/tests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -38,7 +38,7 @@
 from zope.app.location.traversing import LocationPhysicallyLocatable
 
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
     service = zapi.getGlobalService('Utilities')
     service.provideUtility(IDocumentationModule, InterfaceModule(), '')
@@ -49,9 +49,6 @@
                          LocationPhysicallyLocatable)
 
 
-def tearDown():
-    placelesssetup.tearDown()
-
 def makeRegistration(name, interface, component):
     return type('RegistrationStub', (),
                 {'name': name, 'provided': interface,
@@ -72,9 +69,9 @@
 def test_suite():
     return unittest.TestSuite((
         DocTestSuite('zope.app.apidoc.utilitymodule',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         DocTestSuite('zope.app.apidoc.utilitymodule.browser',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         ))
 
 if __name__ == '__main__':

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/viewmodule/tests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/viewmodule/tests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/viewmodule/tests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -34,7 +34,7 @@
 class FooView(object):
     pass
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
 
     ztapi.provideAdapter(ISkinRegistration, ISkinDocumentation,
@@ -55,18 +55,13 @@
     provideInterface('IBrowserRequest', IBrowserRequest)
     ztapi.browserView(IFoo, 'index.html', FooView, layer='default')
 
-    
 
-def tearDown():
-    placelesssetup.tearDown()
-
-
 def test_suite():
     return unittest.TestSuite((
         DocTestSuite('zope.app.apidoc.viewmodule',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         DocTestSuite('zope.app.apidoc.viewmodule.browser',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         ))
 
 if __name__ == '__main__':

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/zcmlmodule/tests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/zcmlmodule/tests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/apidoc/zcmlmodule/tests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -32,7 +32,7 @@
 from zope.app.apidoc.tests import Root
 
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
 
     ztapi.provideAdapter(None, IUniqueId, LocationUniqueId)
@@ -45,7 +45,7 @@
     zope.app.appsetup.appsetup.__config_source = os.path.join(
         os.path.dirname(zope.app.__file__), 'meta.zcml')
 
-def tearDown():
+def tearDown(test):
     placelesssetup.tearDown()
     zope.app.appsetup.appsetup.__config_source = old_source_file    
 

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/form/browser/tests/test_registrations.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/form/browser/tests/test_registrations.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/form/browser/tests/test_registrations.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -59,7 +59,7 @@
 sample = SampleObject()
 vocab = SampleVocabulary([])
 
-def setUp():
+def setUp(test):
     setup.placelessSetUp()
     context = xmlconfig.file("tests/registerWidgets.zcml",
                              zope.app.form.browser)

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/onlinehelp/tests/test_onlinehelp.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/onlinehelp/tests/test_onlinehelp.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/onlinehelp/tests/test_onlinehelp.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -40,7 +40,7 @@
     import zope.app.onlinehelp.tests
     return os.path.dirname(zope.app.onlinehelp.tests.__file__)
 
-def setUp():
+def setUp(tests):
     placelesssetup.setUp()
     ztapi.provideAdapter(None, ITraverser, Traverser)
     ztapi.provideAdapter(None, ITraversable, DefaultTraversable)
@@ -49,9 +49,12 @@
 
 def test_suite():
       return unittest.TestSuite((
-          DocTestSuite('zope.app.onlinehelp', setUp=setUp),
-          DocTestSuite('zope.app.onlinehelp.onlinehelptopic', setUp=setUp),
-          DocTestSuite('zope.app.onlinehelp.onlinehelp', setUp=setUp),
+          DocTestSuite('zope.app.onlinehelp',
+                       setUp=setUp, tearDown=placelesssetup.tearDown),
+          DocTestSuite('zope.app.onlinehelp.onlinehelptopic',
+                       setUp=setUp, tearDown=placelesssetup.tearDown),
+          DocTestSuite('zope.app.onlinehelp.onlinehelp',
+                       setUp=setUp, tearDown=placelesssetup.tearDown),
           ))
 
 if __name__ == '__main__':

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/browser/tests/test_addMenuItem.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/browser/tests/test_addMenuItem.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/browser/tests/test_addMenuItem.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -16,7 +16,7 @@
 >>> context = Context()
 >>> addMenuItem(context, class_=X, title="Add an X",
 ...             permission="zope.ManageContent")
->>> context # doctest: +CONTEXT_DIFF
+>>> context
 ((('utility',
    <InterfaceClass zope.component.interfaces.IFactory>,
    'zope.app.browser.add.zope.app.publisher.browser.tests.test_addMenuItem.X'),
@@ -131,7 +131,7 @@
     >>> addMenuItem(context, class_=X, title="Add an X",
     ...             permission="zope.ManageContent", description="blah blah",
     ...             filter="context/foo", view="AddX")
-    >>> context # doctest: +CONTEXT_DIFF
+    >>> context
     ((('utility',
        <InterfaceClass zope.component.interfaces.IFactory>,
        'zope.app.browser.add.""" \

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/xmlrpc/ftests.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/xmlrpc/ftests.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/publisher/xmlrpc/ftests.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -39,11 +39,11 @@
 name = 'zope.app.publisher.xmlrpc.README'
 
 
-def setUp():
+def setUp(test):
     globs['__name__'] = name    
     sys.modules[name] = FakeModule(globs)
 
-def tearDown():
+def tearDown(test):
     # clean up the views we registered:
     
     # we use the fact that registering None unregisters whatever is

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/securitypolicy/tests/test_zopepolicy.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/securitypolicy/tests/test_zopepolicy.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/securitypolicy/tests/test_zopepolicy.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -37,7 +37,7 @@
      import AnnotationGrantInfo
 from zope.security.management import endInteraction
 
-def setUp():
+def setUp(test):
     placelesssetup.setUp()
     endInteraction()
     ztapi.provideAdapter(
@@ -56,14 +56,12 @@
         IAnnotatable, IGrantInfo,
         AnnotationGrantInfo)
 
-def tearDown():
-    placelesssetup.tearDown()
 
 def test_suite():
     return unittest.TestSuite((
         DocFileSuite('zopepolicy.txt',
                      package='zope.app.securitypolicy',
-                     setUp=setUp, tearDown=tearDown),
+                     setUp=setUp, tearDown=placelesssetup.tearDown),
         ))
 
 if __name__ == '__main__':

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/tests/functional.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/tests/functional.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/tests/functional.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -573,21 +573,21 @@
     kw['package'] = doctest._normalize_module(kw.get('package'))
 
     kwsetUp = kw.get('setUp')
-    def setUp():
+    def setUp(test):
         FunctionalTestSetup().setUp()
         
         if kwsetUp is not None:
-            kwsetUp()
+            kwsetUp(test)
     kw['setUp'] = setUp
 
     kwtearDown = kw.get('tearDown')
-    def tearDown():
+    def tearDown(test):
         if kwtearDown is not None:
-            kwtearDown()
+            kwtearDown(test)
         FunctionalTestSetup().tearDown()
     kw['tearDown'] = tearDown
 
-    kw['optionflags'] = doctest.ELLIPSIS | doctest.CONTEXT_DIFF
+    kw['optionflags'] = doctest.ELLIPSIS | doctest.REPORT_CDIFF
 
     return doctest.DocFileSuite(*paths, **kw)
 

Modified: Zope3/branches/ZopeX3-3.0/src/zope/app/tests/placelesssetup.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/app/tests/placelesssetup.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/app/tests/placelesssetup.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -35,7 +35,7 @@
                      ContainerPlacelessSetup
                      ):
 
-    def setUp(self):
+    def setUp(self, doctesttest=None):
         CAPlacelessSetup.setUp(self)
         ContainerPlacelessSetup.setUp(self)
         EventPlacelessSetup.setUp(self)
@@ -57,5 +57,13 @@
 
 ps = PlacelessSetup()
 setUp = ps.setUp
-tearDown = ps.tearDown
+
+def tearDown():
+    tearDown_ = ps.tearDown
+    def tearDown(doctesttest=None):
+        tearDown_()
+    return tearDown
+
+tearDown = tearDown()
+
 del ps

Modified: Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py
===================================================================
--- Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py	2004-08-28 19:31:22 UTC (rev 27323)
+++ Zope3/branches/ZopeX3-3.0/src/zope/testing/doctest.py	2004-08-28 19:50:47 UTC (rev 27324)
@@ -176,8 +176,10 @@
     'DONT_ACCEPT_BLANKLINE',
     'NORMALIZE_WHITESPACE',
     'ELLIPSIS',
-    'UNIFIED_DIFF',
-    'CONTEXT_DIFF',
+    'REPORT_UDIFF',
+    'REPORT_CDIFF',
+    'REPORT_NDIFF',
+    'REPORT_ONLY_FIRST_FAILURE',
     # 1. Utility Functions
     'is_private',
     # 2. Example & DocTest
@@ -219,6 +221,11 @@
 import warnings
 from StringIO import StringIO
 
+# Don't whine about the deprecated is_private function in this
+# module's tests.
+warnings.filterwarnings("ignore", "is_private", DeprecationWarning,
+                        __name__, 0)
+
 real_pdb_set_trace = pdb.set_trace
 
 # There are 4 basic classes:
@@ -251,8 +258,10 @@
 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')
+REPORT_UDIFF = register_optionflag('REPORT_UDIFF')
+REPORT_CDIFF = register_optionflag('REPORT_CDIFF')
+REPORT_NDIFF = register_optionflag('REPORT_NDIFF')
+REPORT_ONLY_FIRST_FAILURE = register_optionflag('REPORT_ONLY_FIRST_FAILURE')
 
 # Special string markers for use in `want` strings:
 BLANKLINE_MARKER = '<BLANKLINE>'
@@ -285,8 +294,6 @@
     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")
@@ -338,25 +345,13 @@
     else:
         raise TypeError("Expected a module, string, or None")
 
-def _tag_msg(tag, msg, indent='    '):
+def _indent(s, indent=4):
     """
-    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 the message is displayed on separate lines,
-    then `indent` is added to the beginning of each line.
+    Add the given number of space characters to the beginning every
+    non-blank line in `s`, and return the result.
     """
-    # If the message doesn't end in a newline, then add one.
-    if msg[-1:] != '\n':
-        msg += '\n'
-    # If the message is short enough, and contains no internal
-    # newlines, then display it on the same line as the tag.
-    # Otherwise, display the tag on its own line.
-    if (len(tag) + len(msg) < 75 and
-        msg.find('\n', 0, len(msg)-1) == -1):
-        return '%s: %s' % (tag, msg)
-    else:
-        msg = '\n'.join([indent+l for l in msg[:-1].split('\n')])
-        return '%s:\n%s\n' % (tag, msg)
+    # This regexp matches the start of non-blank lines:
+    return re.sub('(?m)^(?!$)', indent*' ', s)
 
 def _exception_traceback(exc_info):
     """
@@ -439,6 +434,33 @@
 
     return True
 
+def _comment_line(line):
+    "Return a commented form of the given line"
+    line = line.rstrip()
+    if line:
+        return '# '+line
+    else:
+        return '#'
+
+class _OutputRedirectingPdb(pdb.Pdb):
+    """
+    A specialized version of the python debugger that redirects stdout
+    to a given stream when interacting with the user.  Stdout is *not*
+    redirected when traced code is executed.
+    """
+    def __init__(self, out):
+        self.__out = out
+        pdb.Pdb.__init__(self)
+
+    def trace_dispatch(self, *args):
+        # Redirect stdout to the given stream.
+        save_stdout = sys.stdout
+        sys.stdout = self.__out
+        # Call Pdb's trace dispatch method.
+        pdb.Pdb.trace_dispatch(self, *args)
+        # Restore stdout.
+        sys.stdout = save_stdout
+
 ######################################################################
 ## 2. Example & DocTest
 ######################################################################
@@ -464,6 +486,14 @@
         with a newline unless it's empty, in which case it's an empty
         string.  The constructor adds a newline if needed.
 
+      - exc_msg: The exception message generated by the example, if
+        the example is expected to generate an exception; or `None` if
+        it is not expected to generate an exception.  This exception
+        message is compared against the return value of
+        `traceback.format_exception_only()`.  `exc_msg` ends with a
+        newline unless it's `None`.  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.
@@ -478,12 +508,15 @@
         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):
+    def __init__(self, source, want, exc_msg=None, lineno=0, indent=0,
+                 options=None):
         # Normalize inputs.
         if not source.endswith('\n'):
             source += '\n'
         if want and not want.endswith('\n'):
             want += '\n'
+        if exc_msg is not None and not exc_msg.endswith('\n'):
+            exc_msg += '\n'
         # Store properties.
         self.source = source
         self.want = want
@@ -491,6 +524,7 @@
         self.indent = indent
         if options is None: options = {}
         self.options = options
+        self.exc_msg = exc_msg
 
 class DocTest:
     """
@@ -574,10 +608,71 @@
                   )*)
         ''', re.MULTILINE | re.VERBOSE)
 
+    # A regular expression for handling `want` strings that contain
+    # expected exceptions.  It divides `want` into three pieces:
+    #    - the traceback header line (`hdr`)
+    #    - the traceback stack (`stack`)
+    #    - 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"""
+        # 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 the header.
+        (?P<stack> .*?)      # don't blink: absorb stuff until...
+        ^ (?P<msg> \w+ .*)   #     a line *starts* with alphanum.
+        """, re.VERBOSE | re.MULTILINE | re.DOTALL)
+
     # 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 parse(self, string, name='<string>'):
+        """
+        Divide the given string into examples and intervening text,
+        and return them as a list of alternating Examples and strings.
+        Line numbers for the Examples are 0-based.  The optional
+        argument `name` is a name identifying this string, and is only
+        used for error messages.
+        """
+        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')])
+
+        output = []
+        charno, lineno = 0, 0
+        # Find all doctest examples in the string:
+        for m in self._EXAMPLE_RE.finditer(string):
+            # Add the pre-example text to `output`.
+            output.append(string[charno:m.start()])
+            # Update lineno (lines before this example)
+            lineno += string.count('\n', charno, m.start())
+            # Extract info from the regexp match.
+            (source, options, want, exc_msg) = \
+                     self._parse_example(m, name, lineno)
+            # Create an Example, and add it to the list.
+            if not self._IS_BLANK_OR_COMMENT(source):
+                output.append( Example(source, want, exc_msg,
+                                    lineno=lineno,
+                                    indent=min_indent+len(m.group('indent')),
+                                    options=options) )
+            # Update lineno (lines inside this example)
+            lineno += string.count('\n', m.start(), m.end())
+            # Update charno.
+            charno = m.end()
+        # Add any remaining post-example text to `output`.
+        output.append(string[charno:])
+        return output
+
     def get_doctest(self, string, globs, name, filename, lineno):
         """
         Extract all doctest examples from the given string, and
@@ -600,123 +695,10 @@
 
         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:
-        ...        ...     print x
-        ...        ...     print y
-        ...        2
-        ...        3
-        ...
-        ...        Some text.
-        ...        >>> x+y
-        ...        5
-        ...        '''
-        >>> 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\\n', '5\\n', 9)
         """
-        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
+        return [x for x in self.parse(string, name)
+                if isinstance(x, Example)]
 
-    def get_program(self, string, name="<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:\"
-        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:
-        ...        ...     print x
-        ...        ...     print y
-        ...        2
-        ...        3
-        ...
-        ...        Some text.
-        ...        >>> x+y
-        ...        5
-        ...        '''
-        >>> print DocTestParser().get_program(text)
-        x, y = 2, 3  # no output expected
-        if 1:
-            print x
-            print y
-        # Expected:
-        ## 2
-        ## 3
-        #
-        # Some text.
-        x+y
-        # Expected:
-        ## 5
-        """
-        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')])
-
-        output = []
-        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)
-
-            # 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')])
-
-            # 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)
-
     def _parse_example(self, m, name, lineno):
         """
         Given a regular expression match from `_EXAMPLE_RE` (`m`),
@@ -735,26 +717,32 @@
         # 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)
+        self._check_prefix(source_lines[1:], ' '*indent + '.', name, lineno)
         source = '\n'.join([sl[indent+4:] for sl in source_lines])
 
-        # Divide want into lines; check that it's properly
-        # indented; and then strip the indentation.
+        # Divide want into lines; check that it's properly indented; and
+        # then strip the indentation.  Spaces before the last newline should
+        # be preserved, so plain rstrip() isn't good enough.
         want = m.group('want')
-
-        # 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')
+        if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
+            del want_lines[-1]  # forget final newline & spaces after it
         self._check_prefix(want_lines, ' '*indent, name,
-                           lineno+len(source_lines))
+                           lineno + len(source_lines))
         want = '\n'.join([wl[indent:] for wl in want_lines])
 
-        return source, want
+        # If `want` contains a traceback message, then extract it.
+        m = self._EXCEPTION_RE.match(want)
+        if m:
+            exc_msg = m.group('msg')
+        else:
+            exc_msg = None
 
+        # Extract options from the source.
+        options = self._find_options(source, name, lineno)
+
+        return source, options, want, exc_msg
+
     # 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
@@ -793,19 +781,15 @@
 
     # This regular expression finds the indentation of every non-blank
     # line in a string.
-    _INDENT_RE = re.compile('^([ ]+)(?=\S)', re.MULTILINE)
+    _INDENT_RE = re.compile('^([ ]*)(?=\S)', re.MULTILINE)
 
     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)])
-
-    def _comment_line(self, line):
-        "Return a commented form of the given line"
-        line = line.rstrip()
-        if line:
-            return '# '+line
+        indents = [len(indent) for indent in self._INDENT_RE.findall(s)]
+        if len(indents) > 0:
+            return min(indents)
         else:
-            return '#'
+            return 0
 
     def _check_prompt_blank(self, lines, indent, name, lineno):
         """
@@ -1229,8 +1213,12 @@
         example.  (Only displays a message if verbose=True)
         """
         if self._verbose:
-            out(_tag_msg("Trying", example.source) +
-                _tag_msg("Expecting", example.want or "nothing"))
+            if example.want:
+                out('Trying:\n' + _indent(example.source) +
+                    'Expecting:\n' + _indent(example.want))
+            else:
+                out('Trying:\n' + _indent(example.source) +
+                    'Expecting nothing\n')
 
     def report_success(self, out, test, example, got):
         """
@@ -1244,17 +1232,15 @@
         """
         Report that the given example failed.
         """
-        # Print an error message.
         out(self._failure_header(test, example) +
-            self._checker.output_difference(example.want, got,
-                                            self.optionflags))
+            self._checker.output_difference(example, got, self.optionflags))
 
     def report_unexpected_exception(self, out, test, example, exc_info):
         """
         Report that the given example raised an unexpected exception.
         """
         out(self._failure_header(test, example) +
-            _tag_msg("Exception raised", _exception_traceback(exc_info)))
+            'Exception raised:\n' + _indent(_exception_traceback(exc_info)))
 
     def _failure_header(self, test, example):
         out = [self.DIVIDER]
@@ -1269,38 +1255,13 @@
             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'
+        out.append(_indent(source))
+        return '\n'.join(out)
 
     #/////////////////////////////////////////////////////////////////
     # DocTest Running
     #/////////////////////////////////////////////////////////////////
 
-    # A regular expression for handling `want` strings that contain
-    # 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)
-
     def __run(self, test, compileflags, out):
         """
         Run the examples in `test`.  Write the outcome of each example
@@ -1319,7 +1280,13 @@
         original_optionflags = self.optionflags
 
         # Process each example.
-        for example in test.examples:
+        for examplenum, example in enumerate(test.examples):
+
+            # If REPORT_ONLY_FIRST_FAILURE is set, then supress
+            # reporting after the first failure.
+            quiet = (self.optionflags & REPORT_ONLY_FIRST_FAILURE and
+                     failures > 0)
+
             # Merge in the example's options.
             self.optionflags = original_optionflags
             if example.options:
@@ -1331,20 +1298,28 @@
 
             # Record that we started this example.
             tries += 1
-            self.report_start(out, test, example)
+            if not quiet:
+                self.report_start(out, test, example)
 
+            # Use a special filename for compile(), so we can retrieve
+            # the source code during interactive debugging (see
+            # __patched_linecache_getlines).
+            filename = '<doctest %s[%d]>' % (test.name, examplenum)
+
             # Run the example in the given context (globs), and record
             # any exception that gets raised.  (But don't intercept
             # keyboard interrupts.)
             try:
                 # Don't blink!  This is where the user's code gets run.
-                exec compile(example.source, "<string>", "single",
+                exec compile(example.source, filename, "single",
                              compileflags, 1) in test.globs
+                self.debugger.set_continue() # ==== Example Finished ====
                 exception = None
             except KeyboardInterrupt:
                 raise
             except:
                 exception = sys.exc_info()
+                self.debugger.set_continue() # ==== Example Finished ====
 
             got = self._fakeout.getvalue()  # the actual output
             self._fakeout.truncate(0)
@@ -1354,9 +1329,11 @@
             if exception is None:
                 if self._checker.check_output(example.want, got,
                                               self.optionflags):
-                    self.report_success(out, test, example, got)
+                    if not quiet:
+                        self.report_success(out, test, example, got)
                 else:
-                    self.report_failure(out, test, example, got)
+                    if not quiet:
+                        self.report_failure(out, test, example, got)
                     failures += 1
 
             # If the example raised an exception, then check if it was
@@ -1365,28 +1342,26 @@
                 exc_info = sys.exc_info()
                 exc_msg = traceback.format_exception_only(*exc_info[:2])[-1]
 
-                # Search the `want` string for an exception.  If we don't
-                # find one, then report an unexpected exception.
-                m = self._EXCEPTION_RE.match(example.want)
-                if m is None:
-                    self.report_unexpected_exception(out, test, example,
-                                                     exc_info)
+                # If `example.exc_msg` is None, then we weren't
+                # expecting an exception.
+                if example.exc_msg is None:
+                    if not quiet:
+                        self.report_unexpected_exception(out, test, example,
+                                                         exc_info)
                     failures += 1
+                # If `example.exc_msg` matches the actual exception
+                # message (`exc_msg`), then the example succeeds.
+                elif (self._checker.check_output(example.exc_msg, exc_msg,
+                                                 self.optionflags)):
+                    if not quiet:
+                        got += _exception_traceback(exc_info)
+                        self.report_success(out, test, example, got)
+                # Otherwise, the example fails.
                 else:
-                    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._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 + _exception_traceback(exc_info))
-                    else:
-                        self.report_failure(out, test, example,
-                                       got + _exception_traceback(exc_info))
-                        failures += 1
+                    if not quiet:
+                        got += _exception_traceback(exc_info)
+                        self.report_failure(out, test, example, got)
+                    failures += 1
 
         # Restore the option flags (in case they were modified)
         self.optionflags = original_optionflags
@@ -1405,6 +1380,17 @@
         self.failures += f
         self.tries += t
 
+    __LINECACHE_FILENAME_RE = re.compile(r'<doctest '
+                                         r'(?P<name>[\w\.]+)'
+                                         r'\[(?P<examplenum>\d+)\]>$')
+    def __patched_linecache_getlines(self, filename):
+        m = self.__LINECACHE_FILENAME_RE.match(filename)
+        if m and m.group('name') == self.test.name:
+            example = self.test.examples[int(m.group('examplenum'))]
+            return example.source.splitlines(True)
+        else:
+            return self.save_linecache_getlines(filename)
+
     def run(self, test, compileflags=None, out=None, clear_globs=True):
         """
         Run the examples in `test`, and display the results using the
@@ -1425,6 +1411,8 @@
         `DocTestRunner.check_output`, and the results are formatted by
         the `DocTestRunner.report_*` methods.
         """
+        self.test = test
+
         if compileflags is None:
             compileflags = _extract_future_flags(test.globs)
 
@@ -1433,25 +1421,27 @@
             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()
-
+        # Patch pdb.set_trace to restore sys.stdout during interactive
+        # debugging (so it's not still redirected to self._fakeout).
+        # Note that the interactive output will go to *our*
+        # save_stdout, even if that's not the real sys.stdout; this
+        # allows us to write test cases for the set_trace behavior.
         save_set_trace = pdb.set_trace
-        pdb.set_trace = set_trace
+        self.debugger = _OutputRedirectingPdb(save_stdout)
+        self.debugger.reset()
+        pdb.set_trace = self.debugger.set_trace
+
+        # Patch linecache.getlines, so we can see the example's source
+        # when we're inside the debugger.
+        self.save_linecache_getlines = linecache.getlines
+        linecache.getlines = self.__patched_linecache_getlines
+
         try:
             return self.__run(test, compileflags, out)
         finally:
             sys.stdout = save_stdout
             pdb.set_trace = save_set_trace
+            linecache.getlines = self.save_linecache_getlines
             if clear_globs:
                 test.globs.clear()
 
@@ -1557,7 +1547,7 @@
 
         # 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.
+        # in conjunction with the ELLIPSIS flag.
         if optionflags & NORMALIZE_WHITESPACE:
             got = ' '.join(got.split())
             want = ' '.join(want.split())
@@ -1573,54 +1563,77 @@
         # We didn't find any match; return false.
         return False
 
-    def output_difference(self, want, got, optionflags):
+    # Should we do a fancy diff?
+    def _do_a_fancy_diff(self, want, got, optionflags):
+        # Not unless they asked for a fancy diff.
+        if not optionflags & (REPORT_UDIFF |
+                              REPORT_CDIFF |
+                              REPORT_NDIFF):
+            return False
+
+        # If expected output uses ellipsis, a meaningful fancy diff is
+        # too hard ... or maybe not.  In two real-life failures Tim saw,
+        # a diff was a major help anyway, so this is commented out.
+        # [todo] _ellipsis_match() knows which pieces do and don't match,
+        # and could be the basis for a kick-ass diff in this case.
+        ##if optionflags & ELLIPSIS and ELLIPSIS_MARKER in want:
+        ##    return False
+
+        # ndiff does intraline difference marking, so can be useful even
+        # for 1-line differences.
+        if optionflags & REPORT_NDIFF:
+            return True
+
+        # The other diff types need at least a few lines to be helpful.
+        return want.count('\n') > 2 and got.count('\n') > 2
+
+    def output_difference(self, example, 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.
+        expected output for a given example (`example`) and the actual
+        output (`got`).  `optionflags` is the set of option flags used
+        to compare `want` and `got`.
         """
-        
+        want = example.want
         # 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)):
+        # Check if we should use diff.
+        if self._do_a_fancy_diff(want, got, optionflags):
             # 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')]
+            want_lines = want.splitlines(True)  # True == keep line ends
+            got_lines = got.splitlines(True)
             # 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'
+            if optionflags & REPORT_UDIFF:
+                diff = difflib.unified_diff(want_lines, got_lines, n=2)
+                diff = list(diff)[2:] # strip the diff header
+                kind = 'unified diff with -expected +actual'
+            elif optionflags & REPORT_CDIFF:
+                diff = difflib.context_diff(want_lines, got_lines, n=2)
+                diff = list(diff)[2:] # strip the diff header
+                kind = 'context diff with expected followed by actual'
+            elif optionflags & REPORT_NDIFF:
+                engine = difflib.Differ(charjunk=difflib.IS_CHARACTER_JUNK)
+                diff = list(engine.compare(want_lines, got_lines))
+                kind = 'ndiff with -expected +actual'
             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))
+            return 'Differences (%s):\n' % kind + _indent(''.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)
+        if want and got:
+            return 'Expected:\n%sGot:\n%s' % (_indent(want), _indent(got))
+        elif want:
+            return 'Expected:\n%sGot nothing\n' % _indent(want)
+        elif got:
+            return 'Expected nothing\nGot:\n%s' % _indent(got)
+        else:
+            return 'Expected nothing\nGot nothing\n'
 
 class DocTestFailure(Exception):
     """A DocTest example has failed in debugging mode.
@@ -1808,43 +1821,18 @@
     detailed, else very brief (in fact, empty if all tests passed).
 
     Optional keyword arg "optionflags" or's together module constants,
-    and defaults to 0.  This is new in 2.3.  Possible values:
+    and defaults to 0.  This is new in 2.3.  Possible values (see the
+    docs for details):
 
         DONT_ACCEPT_TRUE_FOR_1
-            By default, if an expected output block contains just "1",
-            an actual output block containing just "True" is considered
-            to be a match, and similarly for "0" versus "False".  When
-            DONT_ACCEPT_TRUE_FOR_1 is specified, neither substitution
-            is allowed.
-
         DONT_ACCEPT_BLANKLINE
-            By default, if an expected output block contains a line
-            containing only the string "<BLANKLINE>", then that line
-            will match a blank line in the actual output.  When
-            DONT_ACCEPT_BLANKLINE is specified, this substitution is
-            not allowed.
-
         NORMALIZE_WHITESPACE
-            When NORMALIZE_WHITESPACE is specified, all sequences of
-            whitespace are treated as equal.  I.e., any sequence of
-            whitespace within the expected output will match any
-            sequence of whitespace within the actual output.
-
         ELLIPSIS
-            When ELLIPSIS is specified, then an ellipsis marker
-            ("...") in the expected output can match any substring in
-            the actual output.
+        REPORT_UDIFF
+        REPORT_CDIFF
+        REPORT_NDIFF
+        REPORT_ONLY_FIRST_FAILURE
 
-        UNIFIED_DIFF
-            When UNIFIED_DIFF is specified, failures that involve
-            multi-line expected and actual outputs will be displayed
-            using a unified diff.
-
-        CONTEXT_DIFF
-            When CONTEXT_DIFF is specified, failures that involve
-            multi-line expected and actual outputs will be displayed
-            using a context diff.
-
     Optional keyword arg "raise_on_error" raises an exception on the
     first unexpected exception or failure. This allows failures to be
     post-mortem debugged.
@@ -2004,6 +1992,65 @@
 ## 8. Unittest Support
 ######################################################################
 
+_unittest_reportflags = 0
+valid_unittest_reportflags = (
+    REPORT_CDIFF |
+    REPORT_UDIFF |
+    REPORT_NDIFF |
+    REPORT_ONLY_FIRST_FAILURE
+    )
+def set_unittest_reportflags(flags):
+    """Sets the unit test option flags
+
+    The old flag is returned so that a runner could restore the old
+    value if it wished to:
+
+      >>> old = _unittest_reportflags
+      >>> set_unittest_reportflags(REPORT_NDIFF |
+      ...                          REPORT_ONLY_FIRST_FAILURE) == old
+      True
+
+      >>> import doctest
+      >>> doctest._unittest_reportflags == (REPORT_NDIFF |
+      ...                                   REPORT_ONLY_FIRST_FAILURE)
+      True
+      
+    Only reporting flags can be set:
+
+      >>> set_unittest_reportflags(ELLIPSIS)
+      Traceback (most recent call last):
+      ...
+      ValueError: ('Invalid flags passed', 8)
+
+      >>> set_unittest_reportflags(old) == (REPORT_NDIFF |
+      ...                                   REPORT_ONLY_FIRST_FAILURE)
+      True
+
+    """
+
+    # extract the valid reporting flags:
+    rflags = flags & valid_unittest_reportflags
+
+    # Now remove these flags from the given flags
+    nrflags = flags ^ rflags
+
+    if nrflags:
+        raise ValueError("Invalid flags passed", flags)
+    
+    global _unittest_reportflags
+    old = _unittest_reportflags
+    _unittest_reportflags = flags
+    return old
+    
+
+class FakeModule:
+    """Fake module created by tests
+    """
+    
+    def __init__(self, dict, name):
+        self.__dict__ = dict
+        self.__name__ = name
+
 class DocTestCase(unittest.TestCase):
 
     def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
@@ -2017,23 +2064,37 @@
         self._dt_tearDown = tearDown
 
     def setUp(self):
+        test = self._dt_test
+            
         if self._dt_setUp is not None:
-            self._dt_setUp()
+            self._dt_setUp(test)
 
     def tearDown(self):
+        test = self._dt_test
+
         if self._dt_tearDown is not None:
-            self._dt_tearDown()
+            self._dt_tearDown(test)
 
+        test.globs.clear()
+
     def runTest(self):
         test = self._dt_test
         old = sys.stdout
         new = StringIO()
-        runner = DocTestRunner(optionflags=self._dt_optionflags,
+        optionflags = self._dt_optionflags
+        
+        if not (optionflags & valid_unittest_reportflags):
+            # The option flags don't include any reporting flags,
+            # so add the default reporting flags
+            optionflags |= _unittest_reportflags
+        
+        runner = DocTestRunner(optionflags=optionflags,
                                checker=self._dt_checker, verbose=False)
 
         try:
             runner.DIVIDER = "-"*70
-            failures, tries = runner.run(test, out=new.write)
+            failures, tries = runner.run(
+                test, out=new.write, clear_globs=False)
         finally:
             sys.stdout = old
 
@@ -2136,12 +2197,10 @@
     def shortDescription(self):
         return "Doctest: " + self._dt_test.name
 
-def DocTestSuite(module=None, globs=None, extraglobs=None,
-                 optionflags=0, test_finder=None,
-                 setUp=lambda: None, tearDown=lambda: None,
-                 checker=None):
+def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None,
+                 **options):
     """
-    Convert doctest tests for a mudule to a unittest test suite.
+    Convert doctest tests for a module to a unittest test suite.
 
     This converts each documentation string in a module that
     contains doctest tests to a unittest test case.  If any of the
@@ -2153,6 +2212,32 @@
     can be either a module or a module name.
 
     If no argument is given, the calling module is used.
+
+    A number of options may be provided as keyword arguments:
+
+    package
+      The name of a Python package.  Text-file paths will be
+      interpreted relative to the directory containing this package.
+      The package may be supplied as a package object or as a dotted
+      package name.
+
+    setUp
+      The name of a set-up function.  This is called before running the
+      tests in each file. The setUp function will be passed a DocTest
+      object.  The setUp function can access the test globals as the
+      globs attribute of the test passed.
+
+    tearDown
+      The name of a tear-down function.  This is called after running the
+      tests in each file.  The tearDown function will be passed a DocTest
+      object.  The tearDown function can access the test globals as the
+      globs attribute of the test passed.
+
+    globs
+      A dictionary containing initial global variables for the tests.
+
+    optionflags
+       A set of doctest option flags expressed as an integer.
     """
 
     if test_finder is None:
@@ -2162,7 +2247,9 @@
     tests = test_finder.find(module, globs=globs, extraglobs=extraglobs)
     if globs is None:
         globs = module.__dict__
-    if not tests: # [XX] why do we want to do this?
+    if not tests:
+        # Why do we want to do this? Because it reveals a bug that might
+        # otherwise be hidden.
         raise ValueError(module, "has no tests")
 
     tests.sort()
@@ -2175,8 +2262,7 @@
             if filename[-4:] in (".pyc", ".pyo"):
                 filename = filename[:-1]
             test.filename = filename
-        suite.addTest(DocTestCase(test, optionflags, setUp, tearDown,
-                                  checker))
+        suite.addTest(DocTestCase(test, **options))
 
     return suite
 
@@ -2194,9 +2280,7 @@
                 % (self._dt_test.name, self._dt_test.filename, err)
                 )
 
-def DocFileTest(path, package=None, globs=None,
-                setUp=None, tearDown=None,
-                optionflags=0):
+def DocFileTest(path, package=None, globs=None, **options):
     package = _normalize_module(package)
     name = path.split('/')[-1]
     dir = os.path.split(package.__file__)[0]
@@ -2208,7 +2292,7 @@
 
     test = DocTestParser().get_doctest(doc, globs, name, path, 0)
 
-    return DocFileCase(test, optionflags, setUp, tearDown)
+    return DocFileCase(test, **options)
 
 def DocFileSuite(*paths, **kw):
     """Creates a suite of doctest files.
@@ -2228,14 +2312,22 @@
 
     setUp
       The name of a set-up function.  This is called before running the
-      tests in each file.
+      tests in each file. The setUp function will be passed a DocTest
+      object.  The setUp function can access the test globals as the
+      globs attribute of the test passed.
 
     tearDown
       The name of a tear-down function.  This is called after running the
-      tests in each file.
+      tests in each file.  The tearDown function will be passed a DocTest
+      object.  The tearDown function can access the test globals as the
+      globs attribute of the test passed.
 
     globs
       A dictionary containing initial global variables for the tests.
+
+    optionflags
+       A set of doctest option flags expressed as an integer.
+      
     """
     suite = unittest.TestSuite()
 
@@ -2307,26 +2399,32 @@
        if 0:
           blah
           blah
-       <BLANKLINE>
        #
        #     Ho hum
        """
+    output = []
+    for piece in DocTestParser().parse(s):
+        if isinstance(piece, Example):
+            # Add the example's source code (strip trailing NL)
+            output.append(piece.source[:-1])
+            # Add the expected output:
+            want = piece.want
+            if want:
+                output.append('# Expected:')
+                output += ['## '+l for l in want.split('\n')[:-1]]
+        else:
+            # Add non-example text.
+            output += [_comment_line(l)
+                       for l in piece.split('\n')[:-1]]
 
-    return DocTestParser().get_program(s)
+    # 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)
 
-def _want_comment(example):
-    """
-    Return a comment containing the expected output for the given example.
-    """
-    # Return the expected output, if any
-    want = example.want
-    if want:
-        if want[-1] == '\n':
-            want = want[:-1]
-        want = "\n#     ".join(want.split("\n"))
-        want = "\n# Expected:\n#     %s" % want
-    return want
-
 def testsource(module, name):
     """Extract the test sources from a doctest docstring as a script.
 
@@ -2352,27 +2450,34 @@
     "Debug a test script.  `src` is the script, as a string."
     import pdb
 
-    srcfilename = tempfile.mktemp("doctestdebug.py")
+    # Note that tempfile.NameTemporaryFile() cannot be used.  As the
+    # docs say, a file so created cannot be opened by name a second time
+    # on modern Windows boxes, and execfile() needs to open it.
+    srcfilename = tempfile.mktemp(".py", "doctestdebug")
     f = open(srcfilename, 'w')
     f.write(src)
     f.close()
 
-    if globs:
-        globs = globs.copy()
-    else:
-        globs = {}
+    try:
+        if globs:
+            globs = globs.copy()
+        else:
+            globs = {}
 
-    if pm:
-        try:
-            execfile(srcfilename, globs, globs)
-        except:
-            print sys.exc_info()[1]
-            pdb.post_mortem(sys.exc_info()[2])
-    else:
-        # Note that %r is vital here.  '%s' instead can, e.g., cause
-        # backslashes to get treated as metacharacters on Windows.
-        pdb.run("execfile(%r)" % srcfilename, globs, globs)
+        if pm:
+            try:
+                execfile(srcfilename, globs, globs)
+            except:
+                print sys.exc_info()[1]
+                pdb.post_mortem(sys.exc_info()[2])
+        else:
+            # Note that %r is vital here.  '%s' instead can, e.g., cause
+            # backslashes to get treated as metacharacters on Windows.
+            pdb.run("execfile(%r)" % srcfilename, globs, globs)
 
+    finally:
+        os.remove(srcfilename)
+
 def debug(module, name, pm=False):
     """Debug a single doctest docstring.
 
@@ -2438,6 +2543,7 @@
                       >>> x + y, x * y
                       (3, 2)
                       """,
+
             "bool-int equivalence": r"""
                                     In 2.2, boolean expressions displayed
                                     0 or 1.  By default, we still accept
@@ -2453,153 +2559,34 @@
                                     >>> 4 > 4
                                     False
                                     """,
+
             "blank lines": r"""
-            Blank lines can be marked with <BLANKLINE>:
-                >>> print 'foo\n\nbar\n'
-                foo
-                <BLANKLINE>
-                bar
-                <BLANKLINE>
+                Blank lines can be marked with <BLANKLINE>:
+                    >>> print 'foo\n\nbar\n'
+                    foo
+                    <BLANKLINE>
+                    bar
+                    <BLANKLINE>
             """,
-            }
-#             "ellipsis": r"""
-#             If the ellipsis flag is used, then '...' can be used to
-#             elide substrings in the desired output:
-#                 >>> print range(1000)
-#                 [0, 1, 2, ..., 999]
-#             """,
-#             "whitespace normalization": r"""
-#             If the whitespace normalization flag is used, then
-#             differences in whitespace are ignored.
-#                 >>> print range(30)
-#                 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
-#                  15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
-#                  27, 28, 29]
-#             """,
-#            }
 
-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'''
-...      >>> x = x * 2
-...      >>> print x
-...      42
-... ''', 'XYZ')
-**********************************************************************
-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)
->>> t.summarize()
-**********************************************************************
-1 items had failures:
-   1 of   2 in XYZ
-***Test Failed*** 1 failures.
-(1, 4)
->>> t.summarize(verbose=1)
-1 items passed all tests:
-   2 tests in example2
-**********************************************************************
-1 items had failures:
-   1 of   2 in XYZ
-4 tests in 2 items.
-3 passed and 1 failed.
-***Test Failed*** 1 failures.
-(1, 4)
-"""
+            "ellipsis": r"""
+                If the ellipsis flag is used, then '...' can be used to
+                elide substrings in the desired output:
+                    >>> print range(1000) #doctest: +ELLIPSIS
+                    [0, 1, 2, ..., 999]
+            """,
 
-def test2(): r"""
-        >>> warnings.filterwarnings("ignore", "class Tester",
-        ...                         DeprecationWarning, "doctest", 0)
-        >>> t = Tester(globs={}, verbose=1)
-        >>> test = r'''
-        ...    # just an example
-        ...    >>> x = 1 + 2
-        ...    >>> x
-        ...    3
-        ... '''
-        >>> t.runstring(test, "Example")
-        Running string Example
-        Trying: x = 1 + 2
-        Expecting: nothing
-        ok
-        Trying: x
-        Expecting: 3
-        ok
-        0 of 2 examples failed in string Example
-        (0, 2)
-"""
-def test3(): r"""
-        >>> warnings.filterwarnings("ignore", "class Tester",
-        ...                         DeprecationWarning, "doctest", 0)
-        >>> t = Tester(globs={}, verbose=0)
-        >>> def _f():
-        ...     '''Trivial docstring example.
-        ...     >>> assert 2 == 2
-        ...     '''
-        ...     return 32
-        ...
-        >>> t.rundoc(_f)  # expect 0 failures in 1 example
-        (0, 1)
-"""
-def test4(): """
-        >>> import new
-        >>> m1 = new.module('_m1')
-        >>> m2 = new.module('_m2')
-        >>> test_data = \"""
-        ... def _f():
-        ...     '''>>> assert 1 == 1
-        ...     '''
-        ... def g():
-        ...    '''>>> assert 2 != 1
-        ...    '''
-        ... class H:
-        ...    '''>>> assert 2 > 1
-        ...    '''
-        ...    def bar(self):
-        ...        '''>>> assert 1 < 2
-        ...        '''
-        ... \"""
-        >>> exec test_data in m1.__dict__
-        >>> exec test_data in m2.__dict__
-        >>> m1.__dict__.update({"f2": m2._f, "g2": m2.g, "h2": m2.H})
+            "whitespace normalization": r"""
+                If the whitespace normalization flag is used, then
+                differences in whitespace are ignored.
+                    >>> print range(30) #doctest: +NORMALIZE_WHITESPACE
+                    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+                     15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
+                     27, 28, 29]
+            """,
+           }
 
-        Tests that objects outside m1 are excluded:
-
-        >>> warnings.filterwarnings("ignore", "class Tester",
-        ...                         DeprecationWarning, "doctest", 0)
-        >>> t = Tester(globs={}, verbose=0)
-        >>> t.rundict(m1.__dict__, "rundict_test", m1)  # f2 and g2 and h2 skipped
-        (0, 4)
-
-        Once more, not excluding stuff outside m1:
-
-        >>> t = Tester(globs={}, verbose=0)
-        >>> t.rundict(m1.__dict__, "rundict_test_pvt")  # None are skipped.
-        (0, 8)
-
-        The exclusion of objects from outside the designated module is
-        meant to be invoked automagically by testmod.
-
-        >>> testmod(m1, verbose=False)
-        (0, 4)
-"""
-
 def _test():
-    #import doctest
-    #doctest.testmod(doctest, verbose=False,
-    #                optionflags=ELLIPSIS | NORMALIZE_WHITESPACE |
-    #                UNIFIED_DIFF)
-    #print '~'*70
     r = unittest.TextTestRunner()
     r.run(DocTestSuite())
 



More information about the Zope3-Checkins mailing list