From a6b68327b2aab767197f513c18abb5db496241d7 Mon Sep 17 00:00:00 2001 From: Edward Loper Date: Thu, 26 Aug 2004 00:05:43 +0000 Subject: Added an "exc_msg" attribute to Example (containing the expected exception message, or None if no exception is expected); and moved exception parsing from DocTestRunner to DocTestParser. This is architecturally cleaner, since it moves all parsing work to DocTestParser; and it should make it easier for code outside DocTestRunner (notably debugging code) to properly handle expected exceptions. --- Lib/doctest.py | 105 ++++++++++++++++++++++++++++------------------- Lib/test/test_doctest.py | 93 +++++++++++++++++++++++++++++++++-------- 2 files changed, 139 insertions(+), 59 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 01f7cb3..2cd96ba 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -469,6 +469,14 @@ class Example: 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. @@ -483,12 +491,15 @@ class Example: 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 @@ -496,6 +507,7 @@ class Example: self.indent = indent if options is None: options = {} self.options = options + self.exc_msg = exc_msg class DocTest: """ @@ -579,6 +591,28 @@ class DocTestParser: )*) ''', 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 Traceback\ \( + (?: most\ recent\ call\ last + | innermost\ last + ) \) : + ) + \s* $ # toss trailing whitespace on the header. + (?P .*?) # don't blink: absorb stuff until... + ^ (?P \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 @@ -631,13 +665,15 @@ class DocTestParser: # 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) + (source, want, exc_msg) = 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) ) + examples.append( Example(source, want, exc_msg, + lineno=lineno, + indent=len(m.group('indent')), + options=options) ) # Update lineno (lines inside this example) lineno += string.count('\n', m.start(), m.end()) # Update charno. @@ -700,7 +736,7 @@ class DocTestParser: lineno += len(lines) # Extract source/want from the regexp match. - (source, want) = self._parse_example(m, name, lineno) + (source, want, exc_msg) = self._parse_example(m, name, lineno) # Display the source output.append(source) # Display the expected output, if any @@ -754,7 +790,14 @@ class DocTestParser: 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 + + return source, want, exc_msg # This regular expression looks for option directives in the # source code of an example. Option directives are comments @@ -1279,28 +1322,6 @@ class DocTestRunner: # DocTest Running #///////////////////////////////////////////////////////////////// - # 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 Traceback\ \( - (?: most\ recent\ call\ last - | innermost\ last - ) \) : - ) - \s* $ # toss trailing whitespace on the header. - (?P .*?) # don't blink: absorb stuff until... - ^ (?P \w+ .*) # a line *starts* with alphanum. - """, re.VERBOSE | re.MULTILINE | re.DOTALL) - def __run(self, test, compileflags, out): """ Run the examples in `test`. Write the outcome of each example @@ -1365,25 +1386,23 @@ class DocTestRunner: 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: + # If `example.exc_msg` is None, then we weren't + # expecting an exception. + if example.exc_msg is None: 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)): + self.report_success(out, test, example, + got + _exception_traceback(exc_info)) + # Otherwise, the example fails. else: - # The test passes iff the expected exception - # message (`m.group('msg')`) matches the actual - # exception message (`exc_msg`). - if (self._checker.check_output(m.group('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 + self.report_failure(out, test, example, + got + _exception_traceback(exc_info)) + failures += 1 # 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 5d0cf90..a9076a6 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -123,46 +123,107 @@ class SampleNewStyleClass(object): def test_Example(): r""" Unit tests for the `Example` class. -Example is a simple container class that holds a source code string, -an expected output string, and a line number (within the docstring): - - >>> example = doctest.Example('print 1', '1\n', 0) - >>> (example.source, example.want, example.lineno) - ('print 1\n', '1\n', 0) - -The `source` string ends in a newline: +Example is a simple container class that holds: + - `source`: A source string. + - `want`: An expected output string. + - `exc_msg`: An expected exception message string (or None if no + exception is expected). + - `lineno`: A line number (within the docstring). + - `indent`: The example's indentation in the input string. + - `options`: An option dictionary, mapping option flags to True or + False. + +These attributes are set by the constructor. `source` and `want` are +required; the other attributes all have default values: + + >>> example = doctest.Example('print 1', '1\n') + >>> (example.source, example.want, example.exc_msg, + ... example.lineno, example.indent, example.options) + ('print 1\n', '1\n', None, 0, 0, {}) + +The first three attributes (`source`, `want`, and `exc_msg`) may be +specified positionally; the remaining arguments should be specified as +keyword arguments: + + >>> exc_msg = 'IndexError: pop from an empty list' + >>> example = doctest.Example('[].pop()', '', exc_msg, + ... lineno=5, indent=4, + ... options={doctest.ELLIPSIS: True}) + >>> (example.source, example.want, example.exc_msg, + ... example.lineno, example.indent, example.options) + ('[].pop()\n', '', 'IndexError: pop from an empty list\n', 5, 4, {8: True}) + +The constructor normalizes the `source` string to end in a newline: Source spans a single line: no terminating newline. - >>> e = doctest.Example('print 1', '1\n', 0) + >>> e = doctest.Example('print 1', '1\n') >>> e.source, e.want ('print 1\n', '1\n') - >>> e = doctest.Example('print 1\n', '1\n', 0) + >>> e = doctest.Example('print 1\n', '1\n') >>> e.source, e.want ('print 1\n', '1\n') Source spans multiple lines: require terminating newline. - >>> e = doctest.Example('print 1;\nprint 2\n', '1\n2\n', 0) + >>> e = doctest.Example('print 1;\nprint 2\n', '1\n2\n') >>> e.source, e.want ('print 1;\nprint 2\n', '1\n2\n') - >>> e = doctest.Example('print 1;\nprint 2', '1\n2\n', 0) + >>> e = doctest.Example('print 1;\nprint 2', '1\n2\n') >>> e.source, e.want ('print 1;\nprint 2\n', '1\n2\n') -The `want` string ends with a newline, unless it's the empty string: + Empty source string (which should never appear in real examples) + >>> e = doctest.Example('', '') + >>> e.source, e.want + ('\n', '') - >>> e = doctest.Example('print 1', '1\n', 0) +The constructor normalizes the `want` string to end in a newline, +unless it's the empty string: + + >>> e = doctest.Example('print 1', '1\n') >>> e.source, e.want ('print 1\n', '1\n') - >>> e = doctest.Example('print 1', '1', 0) + >>> e = doctest.Example('print 1', '1') >>> e.source, e.want ('print 1\n', '1\n') - >>> e = doctest.Example('print', '', 0) + >>> e = doctest.Example('print', '') >>> e.source, e.want ('print\n', '') + +The constructor normalizes the `exc_msg` string to end in a newline, +unless it's `None`: + + Message spans one line + >>> exc_msg = 'IndexError: pop from an empty list' + >>> e = doctest.Example('[].pop()', '', exc_msg) + >>> e.exc_msg + 'IndexError: pop from an empty list\n' + + >>> exc_msg = 'IndexError: pop from an empty list\n' + >>> e = doctest.Example('[].pop()', '', exc_msg) + >>> e.exc_msg + 'IndexError: pop from an empty list\n' + + Message spans multiple lines + >>> exc_msg = 'ValueError: 1\n 2' + >>> e = doctest.Example('raise ValueError("1\n 2")', '', exc_msg) + >>> e.exc_msg + 'ValueError: 1\n 2\n' + + >>> exc_msg = 'ValueError: 1\n 2\n' + >>> e = doctest.Example('raise ValueError("1\n 2")', '', exc_msg) + >>> e.exc_msg + 'ValueError: 1\n 2\n' + + Empty (but non-None) exception message (which should never appear + in real examples) + >>> exc_msg = '' + >>> e = doctest.Example('raise X()', '', exc_msg) + >>> e.exc_msg + '\n' """ def test_DocTest(): r""" -- cgit v0.12