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.
This commit is contained in:
Edward Loper 2004-08-26 00:05:43 +00:00
parent c5625bac68
commit a6b68327b2
2 changed files with 137 additions and 57 deletions

View File

@ -469,6 +469,14 @@ class Example:
with a newline unless it's empty, in which case it's an empty with a newline unless it's empty, in which case it's an empty
string. The constructor adds a newline if needed. 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 - lineno: The line number within the DocTest string containing
this Example where the Example begins. This line number is this Example where the Example begins. This line number is
zero-based, with respect to the beginning of the DocTest. 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 are left at their default value (as specified by the
DocTestRunner's optionflags). By default, no options are set. 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. # Normalize inputs.
if not source.endswith('\n'): if not source.endswith('\n'):
source += '\n' source += '\n'
if want and not want.endswith('\n'): if want and not want.endswith('\n'):
want += '\n' want += '\n'
if exc_msg is not None and not exc_msg.endswith('\n'):
exc_msg += '\n'
# Store properties. # Store properties.
self.source = source self.source = source
self.want = want self.want = want
@ -496,6 +507,7 @@ class Example:
self.indent = indent self.indent = indent
if options is None: options = {} if options is None: options = {}
self.options = options self.options = options
self.exc_msg = exc_msg
class DocTest: class DocTest:
""" """
@ -579,6 +591,28 @@ class DocTestParser:
)*) )*)
''', re.MULTILINE | re.VERBOSE) ''', 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<hdr> Traceback\ \(
(?: most\ recent\ call\ last
| innermost\ last
) \) :
)
\s* $ # toss trailing whitespace on the header.
(?P<stack> .*?) # don't blink: absorb stuff until...
^ (?P<msg> \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 # A callable returning a true value iff its argument is a blank line
# or contains a single comment. # or contains a single comment.
_IS_BLANK_OR_COMMENT = re.compile(r'^[ ]*(#.*)?$').match _IS_BLANK_OR_COMMENT = re.compile(r'^[ ]*(#.*)?$').match
@ -631,13 +665,15 @@ class DocTestParser:
# Update lineno (lines before this example) # Update lineno (lines before this example)
lineno += string.count('\n', charno, m.start()) lineno += string.count('\n', charno, m.start())
# Extract source/want from the regexp match. # 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. # Extract extra options from the source.
options = self._find_options(source, name, lineno) options = self._find_options(source, name, lineno)
# Create an Example, and add it to the list. # Create an Example, and add it to the list.
if not self._IS_BLANK_OR_COMMENT(source): if not self._IS_BLANK_OR_COMMENT(source):
examples.append( Example(source, want, lineno, examples.append( Example(source, want, exc_msg,
len(m.group('indent')), options) ) lineno=lineno,
indent=len(m.group('indent')),
options=options) )
# Update lineno (lines inside this example) # Update lineno (lines inside this example)
lineno += string.count('\n', m.start(), m.end()) lineno += string.count('\n', m.start(), m.end())
# Update charno. # Update charno.
@ -700,7 +736,7 @@ class DocTestParser:
lineno += len(lines) lineno += len(lines)
# Extract source/want from the regexp match. # 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 # Display the source
output.append(source) output.append(source)
# Display the expected output, if any # Display the expected output, if any
@ -754,7 +790,14 @@ class DocTestParser:
lineno + len(source_lines)) lineno + len(source_lines))
want = '\n'.join([wl[indent:] for wl in want_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 # This regular expression looks for option directives in the
# source code of an example. Option directives are comments # source code of an example. Option directives are comments
@ -1279,28 +1322,6 @@ class DocTestRunner:
# DocTest Running # 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<hdr> Traceback\ \(
(?: most\ recent\ call\ last
| innermost\ last
) \) :
)
\s* $ # toss trailing whitespace on the header.
(?P<stack> .*?) # don't blink: absorb stuff until...
^ (?P<msg> \w+ .*) # a line *starts* with alphanum.
""", re.VERBOSE | re.MULTILINE | re.DOTALL)
def __run(self, test, compileflags, out): def __run(self, test, compileflags, out):
""" """
Run the examples in `test`. Write the outcome of each example Run the examples in `test`. Write the outcome of each example
@ -1365,25 +1386,23 @@ class DocTestRunner:
exc_info = sys.exc_info() exc_info = sys.exc_info()
exc_msg = traceback.format_exception_only(*exc_info[:2])[-1] exc_msg = traceback.format_exception_only(*exc_info[:2])[-1]
# Search the `want` string for an exception. If we don't # If `example.exc_msg` is None, then we weren't
# find one, then report an unexpected exception. # expecting an exception.
m = self._EXCEPTION_RE.match(example.want) if example.exc_msg is None:
if m is None:
self.report_unexpected_exception(out, test, example, self.report_unexpected_exception(out, test, example,
exc_info) exc_info)
failures += 1 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: else:
# The test passes iff the expected exception self.report_failure(out, test, example,
# message (`m.group('msg')`) matches the actual got + _exception_traceback(exc_info))
# exception message (`exc_msg`). failures += 1
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
# Restore the option flags (in case they were modified) # Restore the option flags (in case they were modified)
self.optionflags = original_optionflags self.optionflags = original_optionflags

View File

@ -123,46 +123,107 @@ class SampleNewStyleClass(object):
def test_Example(): r""" def test_Example(): r"""
Unit tests for the `Example` class. Unit tests for the `Example` class.
Example is a simple container class that holds a source code string, Example is a simple container class that holds:
an expected output string, and a line number (within the docstring): - `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.
>>> example = doctest.Example('print 1', '1\n', 0) These attributes are set by the constructor. `source` and `want` are
>>> (example.source, example.want, example.lineno) required; the other attributes all have default values:
('print 1\n', '1\n', 0)
The `source` string ends in a newline: >>> 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. 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 >>> e.source, e.want
('print 1\n', '1\n') ('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 >>> e.source, e.want
('print 1\n', '1\n') ('print 1\n', '1\n')
Source spans multiple lines: require terminating newline. 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 >>> e.source, e.want
('print 1;\nprint 2\n', '1\n2\n') ('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 >>> e.source, e.want
('print 1;\nprint 2\n', '1\n2\n') ('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 >>> e.source, e.want
('print 1\n', '1\n') ('print 1\n', '1\n')
>>> e = doctest.Example('print 1', '1', 0) >>> e = doctest.Example('print 1', '1')
>>> e.source, e.want >>> e.source, e.want
('print 1\n', '1\n') ('print 1\n', '1\n')
>>> e = doctest.Example('print', '', 0) >>> e = doctest.Example('print', '')
>>> e.source, e.want >>> e.source, e.want
('print\n', '') ('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""" def test_DocTest(): r"""