From 1fbf9c5ec10d38d58837e20a681604440aa7b3da Mon Sep 17 00:00:00 2001
From: Tim Peters <tim.peters@gmail.com>
Date: Sat, 4 Sep 2004 17:21:02 +0000
Subject: Added IGNORE_EXCEPTION_DETAIL comparison option.  The need is
 explained in the new docs.

DocTestRunner.__run:  Separate the determination of the example outcome
from reporting that outcome, to squash brittle code duplication and
excessive nesting.
---
 Doc/lib/libdoctest.tex   | 32 ++++++++++++++++++++
 Lib/doctest.py           | 79 ++++++++++++++++++++++++++++--------------------
 Lib/test/test_doctest.py | 37 +++++++++++++++++++++++
 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:
 
-- 
cgit v0.12