[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