summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/doctest.py268
-rw-r--r--Misc/NEWS15
2 files changed, 283 insertions, 0 deletions
diff --git a/Lib/doctest.py b/Lib/doctest.py
index 8bda8d6..55f15f1 100644
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -277,6 +277,10 @@ __all__ = [
'run_docstring_examples',
'is_private',
'Tester',
+ 'DocTestTestFailure',
+ 'DocTestSuite',
+ 'testsource',
+ 'debug',
]
import __future__
@@ -1150,6 +1154,270 @@ def testmod(m=None, name=None, globs=None, verbose=None, isprivate=None,
master.merge(tester)
return failures, tries
+###########################################################################
+# Various doctest extensions, to make using doctest with unittest
+# easier, and to help debugging when a doctest goes wrong. Original
+# code by Jim Fulton.
+
+# Utilities.
+
+# If module is None, return the calling module (the module that called
+# the routine that called _normalize_module -- this normally won't be
+# doctest!). If module is a string, it should be the (possibly dotted)
+# name of a module, and the (rightmost) module object is returned. Else
+# module is returned untouched; the intent appears to be that module is
+# already a module object in this case (although this isn't checked).
+
+def _normalize_module(module):
+ import sys
+
+ if module is None:
+ # Get our caller's caller's module.
+ module = sys._getframe(2).f_globals['__name__']
+ module = sys.modules[module]
+
+ elif isinstance(module, (str, unicode)):
+ # The ["*"] at the end is a mostly meaningless incantation with
+ # a crucial property: if, e.g., module is 'a.b.c', it convinces
+ # __import__ to return c instead of a.
+ module = __import__(module, globals(), locals(), ["*"])
+
+ return module
+
+# tests is a list of (testname, docstring, filename, lineno) tuples.
+# If object has a __doc__ attr, and the __doc__ attr looks like it
+# contains a doctest (specifically, if it contains an instance of '>>>'),
+# then tuple
+# prefix + name, object.__doc__, filename, lineno
+# is appended to tests. Else tests is left alone.
+# There is no return value.
+
+def _get_doctest(name, object, tests, prefix, filename='', lineno=''):
+ doc = getattr(object, '__doc__', '')
+ if isinstance(doc, basestring) and '>>>' in doc:
+ tests.append((prefix + name, doc, filename, lineno))
+
+# tests is a list of (testname, docstring, filename, lineno) tuples.
+# docstrings containing doctests are appended to tests (if any are found).
+# items is a dict, like a module or class dict, mapping strings to objects.
+# mdict is the global dict of a "home" module -- only objects belonging
+# to this module are searched for docstrings. module is the module to
+# which mdict belongs.
+# prefix is a string to be prepended to an object's name when adding a
+# tuple to tests.
+# The objects (values) in items are examined (recursively), and doctests
+# belonging to functions and classes in the home module are appended to
+# tests.
+# minlineno is a gimmick to try to guess the file-relative line number
+# at which a doctest probably begins.
+
+def _extract_doctests(items, module, mdict, tests, prefix, minlineno=0):
+
+ for name, object in items:
+ # Only interested in named objects.
+ if not hasattr(object, '__name__'):
+ continue
+
+ elif hasattr(object, 'func_globals'):
+ # Looks like a function.
+ if object.func_globals is not mdict:
+ # Non-local function.
+ continue
+ code = getattr(object, 'func_code', None)
+ filename = getattr(code, 'co_filename', '')
+ lineno = getattr(code, 'co_firstlineno', -1) + 1
+ if minlineno:
+ minlineno = min(lineno, minlineno)
+ else:
+ minlineno = lineno
+ _get_doctest(name, object, tests, prefix, filename, lineno)
+
+ elif hasattr(object, "__module__"):
+ # Maybe a class-like thing, in which case we care.
+ if object.__module__ != module.__name__:
+ # Not the same module.
+ continue
+ if not (hasattr(object, '__dict__')
+ and hasattr(object, '__bases__')):
+ # Not a class.
+ continue
+
+ lineno = _extract_doctests(object.__dict__.items(),
+ module,
+ mdict,
+ tests,
+ prefix + name + ".")
+ # XXX "-3" is unclear.
+ _get_doctest(name, object, tests, prefix,
+ lineno="%s (or above)" % (lineno - 3))
+
+ return minlineno
+
+# Find all the doctests belonging to the module object.
+# Return a list of
+# (testname, docstring, filename, lineno)
+# tuples.
+
+def _find_tests(module, prefix=None):
+ if prefix is None:
+ prefix = module.__name__
+ mdict = module.__dict__
+ tests = []
+ # Get the module-level doctest (if any).
+ _get_doctest(prefix, module, tests, '', lineno="1 (or above)")
+ # Recursively search the module __dict__ for doctests.
+ if prefix:
+ prefix += "."
+ _extract_doctests(mdict.items(), module, mdict, tests, prefix)
+ return tests
+
+# unittest helpers.
+
+# A function passed to unittest, for unittest to drive.
+# tester is doctest Tester instance. doc is the docstring whose
+# doctests are to be run.
+
+def _utest(tester, name, doc, filename, lineno):
+ import sys
+ from StringIO import StringIO
+
+ old = sys.stdout
+ sys.stdout = new = StringIO()
+ try:
+ failures, tries = tester.runstring(doc, name)
+ finally:
+ sys.stdout = old
+
+ if failures:
+ msg = new.getvalue()
+ lname = '.'.join(name.split('.')[-1:])
+ if not lineno:
+ lineno = "0 (don't know line number)"
+ # Don't change this format! It was designed so that Emacs can
+ # parse it naturally.
+ raise DocTestTestFailure('Failed doctest test for %s\n'
+ ' File "%s", line %s, in %s\n\n%s' %
+ (name, filename, lineno, lname, msg))
+
+class DocTestTestFailure(Exception):
+ """A doctest test failed"""
+
+def DocTestSuite(module=None):
+ """Convert doctest tests for a module to a unittest TestSuite.
+
+ The returned TestSuite is to be run by the unittest framework, and
+ runs each doctest in the module. If any of the doctests fail,
+ then the synthesized unit test fails, and an error is raised showing
+ the name of the file containing the test and a (sometimes approximate)
+ line number.
+
+ The optional module argument provides the module to be tested. It
+ can be a module object or a (possibly dotted) module name. If not
+ specified, the module calling DocTestSuite() is used.
+
+ Example (although note that unittest supplies many ways to use the
+ TestSuite returned; see the unittest docs):
+
+ import unittest
+ import doctest
+ import my_module_with_doctests
+
+ suite = doctest.DocTestSuite(my_module_with_doctests)
+ runner = unittest.TextTestRunner()
+ runner.run(suite)
+ """
+
+ import unittest
+
+ module = _normalize_module(module)
+ tests = _find_tests(module)
+ if not tests:
+ raise ValueError(module, "has no tests")
+
+ tests.sort()
+ suite = unittest.TestSuite()
+ tester = Tester(module)
+ for name, doc, filename, lineno in tests:
+ if not filename:
+ filename = module.__file__
+ if filename.endswith(".pyc"):
+ filename = filename[:-1]
+ elif filename.endswith(".pyo"):
+ filename = filename[:-1]
+ def runit(name=name, doc=doc, filename=filename, lineno=lineno):
+ _utest(tester, name, doc, filename, lineno)
+ suite.addTest(unittest.FunctionTestCase(
+ runit,
+ description="doctest of " + name))
+ return suite
+
+# Debugging support.
+
+def _expect(expect):
+ # Return the expected output (if any), formatted as a Python
+ # comment block.
+ if expect:
+ expect = "\n# ".join(expect.split("\n"))
+ expect = "\n# Expect:\n# %s" % expect
+ return expect
+
+def testsource(module, name):
+ """Extract the doctest examples from a docstring.
+
+ Provide the module (or dotted name of the module) containing the
+ tests to be extracted, and the name (within the module) of the object
+ with the docstring containing the tests to be extracted.
+
+ The doctest examples are returned as a string containing Python
+ code. The expected output blocks in the examples are converted
+ to Python comments.
+ """
+
+ module = _normalize_module(module)
+ tests = _find_tests(module, "")
+ test = [doc for (tname, doc, dummy, dummy) in tests
+ if tname == name]
+ if not test:
+ raise ValueError(name, "not found in tests")
+ test = test[0]
+ examples = [source + _expect(expect)
+ for source, expect, dummy in _extract_examples(test)]
+ return '\n'.join(examples)
+
+def debug(module, name):
+ """Debug a single docstring containing doctests.
+
+ Provide the module (or dotted name of the module) containing the
+ docstring to be debugged, and the name (within the module) of the
+ object with the docstring to be debugged.
+
+ The doctest examples are extracted (see function testsource()),
+ and written to a temp file. The Python debugger (pdb) is then
+ invoked on that file.
+ """
+
+ import os
+ import pdb
+ import tempfile
+
+ module = _normalize_module(module)
+ testsrc = testsource(module, name)
+ srcfilename = tempfile.mktemp("doctestdebug.py")
+ f = file(srcfilename, 'w')
+ f.write(testsrc)
+ f.close()
+
+ globs = {}
+ globs.update(module.__dict__)
+ try:
+ # Note that %r is vital here. '%s' instead can, e.g., cause
+ # backslashes to get treated as metacharacters on Windows.
+ pdb.run("execfile(%r)" % srcfilename, globs, globs)
+ finally:
+ os.remove(srcfilename)
+
+
+
class _TestClass:
"""
A pointless class, for sanity-checking of docstring testing.
diff --git a/Misc/NEWS b/Misc/NEWS
index 9367788..69d63fb 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -86,6 +86,21 @@ Extension modules
Library
-------
+- Some happy doctest extensions from Jim Fulton have been added to
+ doctest.py. These are already being used in Zope3. The two
+ primary ones:
+
+ doctest.debug(module, name) extracts the doctests from the named object
+ in the given module, puts them in a temp file, and starts pdb running
+ on that file. This is great when a doctest fails.
+
+ doctest.DocTestSuite(module=None) returns a synthesized unittest
+ TestSuite instance, to be run by the unittest framework, which
+ runs all the doctests in the module. This allows writing tests in
+ doctest style (which can be clearer and shorter than writing tests
+ in unittest style), without losing unittest's powerful testing
+ framework features (which doctest lacks).
+
- ZipFile.testzip() now only traps BadZipfile exceptions. Previously,
a bare except caught to much and reported all errors as a problem
in the archive.