summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/lib/libdoctest.tex32
-rw-r--r--Lib/doctest.py79
-rw-r--r--Lib/test/test_doctest.py37
3 files changed, 116 insertions, 32 deletions
diff --git a/Doc/lib/libdoctest.tex b/Doc/lib/libdoctest.tex
index bd9bb3d..374f851 100644
--- a/Doc/lib/libdoctest.tex
+++ b/Doc/lib/libdoctest.tex
@@ -307,6 +307,9 @@ Some details you should read once, but won't need to remember:
to be the start of the exception detail. Of course this does the
right thing for genuine tracebacks.
+\item When the \constant{IGNORE_EXCEPTION_DETAIL} doctest option is
+ is specified, everything following the leftmost colon is ignored.
+
\end{itemize}
\versionchanged[The ability to handle a multi-line exception detail
@@ -365,6 +368,34 @@ example's expected output:
is prone to in regular expressions.
\end{datadesc}
+\begin{datadesc}{IGNORE_EXCEPTION_DETAIL}
+ When specified, an example that expects an exception passes if
+ an exception of the expected type is raised, even if the exception
+ detail does not match. For example, an example expecting
+ \samp{ValueError: 42} will pass if the actual exception raised is
+ \samp{ValueError: 3*14}, but will fail, e.g., if
+ \exception{TypeError} is raised.
+
+ Note that a similar effect can be obtained using \constant{ELLIPSIS},
+ and \constant{IGNORE_EXCEPTION_DETAIL} may go away when Python releases
+ prior to 2.4 become uninteresting. Until then,
+ \constant{IGNORE_EXCEPTION_DETAIL} is the only clear way to write a
+ doctest that doesn't care about the exception detail yet continues
+ to pass under Python releases prior to 2.4 (doctest directives
+ appear to be comments to them). For example,
+
+\begin{verbatim}
+>>> (1, 2)[3] = 'moo' #doctest: +IGNORE_EXCEPTION_DETAIL
+Traceback (most recent call last):
+ File "<stdin>", line 1, in ?
+TypeError: object doesn't support item assignment
+\end{verbatim}
+
+ passes under Python 2.4 and Python 2.3. The detail changed in 2.4,
+ to say "does not" instead of "doesn't".
+
+\end{datadesc}
+
\begin{datadesc}{COMPARISON_FLAGS}
A bitmask or'ing together all the comparison flags above.
\end{datadesc}
@@ -463,6 +494,7 @@ can be useful.
\versionchanged[Constants \constant{DONT_ACCEPT_BLANKLINE},
\constant{NORMALIZE_WHITESPACE}, \constant{ELLIPSIS},
+ \constant{IGNORE_EXCEPTION_DETAIL},
\constant{REPORT_UDIFF}, \constant{REPORT_CDIFF},
\constant{REPORT_NDIFF}, \constant{REPORT_ONLY_FIRST_FAILURE},
\constant{COMPARISON_FLAGS} and \constant{REPORTING_FLAGS}
diff --git a/Lib/doctest.py b/Lib/doctest.py
index 0c2787f..d77fe15 100644
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -176,6 +176,7 @@ __all__ = [
'DONT_ACCEPT_BLANKLINE',
'NORMALIZE_WHITESPACE',
'ELLIPSIS',
+ 'IGNORE_EXCEPTION_DETAIL',
'COMPARISON_FLAGS',
'REPORT_UDIFF',
'REPORT_CDIFF',
@@ -261,11 +262,13 @@ DONT_ACCEPT_TRUE_FOR_1 = register_optionflag('DONT_ACCEPT_TRUE_FOR_1')
DONT_ACCEPT_BLANKLINE = register_optionflag('DONT_ACCEPT_BLANKLINE')
NORMALIZE_WHITESPACE = register_optionflag('NORMALIZE_WHITESPACE')
ELLIPSIS = register_optionflag('ELLIPSIS')
+IGNORE_EXCEPTION_DETAIL = register_optionflag('IGNORE_EXCEPTION_DETAIL')
COMPARISON_FLAGS = (DONT_ACCEPT_TRUE_FOR_1 |
DONT_ACCEPT_BLANKLINE |
NORMALIZE_WHITESPACE |
- ELLIPSIS)
+ ELLIPSIS |
+ IGNORE_EXCEPTION_DETAIL)
REPORT_UDIFF = register_optionflag('REPORT_UDIFF')
REPORT_CDIFF = register_optionflag('REPORT_CDIFF')
@@ -1293,6 +1296,10 @@ class DocTestRunner:
# to modify them).
original_optionflags = self.optionflags
+ SUCCESS, FAILURE, BOOM = range(3) # `outcome` state
+
+ check = self._checker.check_output
+
# Process each example.
for examplenum, example in enumerate(test.examples):
@@ -1337,45 +1344,53 @@ class DocTestRunner:
got = self._fakeout.getvalue() # the actual output
self._fakeout.truncate(0)
+ outcome = FAILURE # guilty until proved innocent or insane
# If the example executed without raising any exceptions,
- # then verify its output and report its outcome.
+ # verify its output.
if exception is None:
- if self._checker.check_output(example.want, got,
- self.optionflags):
- if not quiet:
- self.report_success(out, test, example, got)
- else:
- if not quiet:
- self.report_failure(out, test, example, got)
- failures += 1
-
- # If the example raised an exception, then check if it was
- # expected.
+ if check(example.want, got, self.optionflags):
+ outcome = SUCCESS
+
+ # The example raised an exception: check if it was expected.
else:
exc_info = sys.exc_info()
exc_msg = traceback.format_exception_only(*exc_info[:2])[-1]
+ if not quiet:
+ got += _exception_traceback(exc_info)
- # If `example.exc_msg` is None, then we weren't
- # expecting an exception.
+ # 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:
- if not quiet:
- got += _exception_traceback(exc_info)
- self.report_failure(out, test, example, got)
- failures += 1
+ outcome = BOOM
+
+ # We expected an exception: see whether it matches.
+ elif check(example.exc_msg, exc_msg, self.optionflags):
+ outcome = SUCCESS
+
+ # Another chance if they didn't care about the detail.
+ elif self.optionflags & IGNORE_EXCEPTION_DETAIL:
+ m1 = re.match(r'[^:]*:', example.exc_msg)
+ m2 = re.match(r'[^:]*:', exc_msg)
+ if m1 and m2 and check(m1.group(0), m2.group(0),
+ self.optionflags):
+ outcome = SUCCESS
+
+ # Report the outcome.
+ if outcome is SUCCESS:
+ if not quiet:
+ self.report_success(out, test, example, got)
+ elif outcome is FAILURE:
+ if not quiet:
+ self.report_failure(out, test, example, got)
+ failures += 1
+ elif outcome is BOOM:
+ if not quiet:
+ self.report_unexpected_exception(out, test, example,
+ exc_info)
+ failures += 1
+ else:
+ assert False, ("unknown outcome", outcome)
# Restore the option flags (in case they were modified)
self.optionflags = original_optionflags
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
index 2b39e33..7f51ab5 100644
--- a/Lib/test/test_doctest.py
+++ b/Lib/test/test_doctest.py
@@ -837,6 +837,43 @@ message is raised, then it is reported as a failure:
ValueError: message
(1, 1)
+However, IGNORE_EXCEPTION_DETAIL can be used to allow a mismatch in the
+detail:
+
+ >>> def f(x):
+ ... r'''
+ ... >>> raise ValueError, 'message' #doctest: +IGNORE_EXCEPTION_DETAIL
+ ... Traceback (most recent call last):
+ ... ValueError: wrong message
+ ... '''
+ >>> test = doctest.DocTestFinder().find(f)[0]
+ >>> doctest.DocTestRunner(verbose=False).run(test)
+ (0, 1)
+
+But IGNORE_EXCEPTION_DETAIL does not allow a mismatch in the exception type:
+
+ >>> def f(x):
+ ... r'''
+ ... >>> raise ValueError, 'message' #doctest: +IGNORE_EXCEPTION_DETAIL
+ ... Traceback (most recent call last):
+ ... TypeError: wrong type
+ ... '''
+ >>> test = doctest.DocTestFinder().find(f)[0]
+ >>> doctest.DocTestRunner(verbose=False).run(test)
+ ... # doctest: +ELLIPSIS
+ **********************************************************************
+ Line 2, in f
+ Failed example:
+ raise ValueError, 'message' #doctest: +IGNORE_EXCEPTION_DETAIL
+ Expected:
+ Traceback (most recent call last):
+ TypeError: wrong type
+ Got:
+ Traceback (most recent call last):
+ ...
+ ValueError: message
+ (1, 1)
+
If an exception is raised but not expected, then it is reported as an
unexpected exception: