diff options
-rw-r--r-- | Doc/lib/libdoctest.tex | 32 | ||||
-rw-r--r-- | Lib/doctest.py | 79 | ||||
-rw-r--r-- | 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: |