mirror of https://github.com/python/cpython
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:
parent
c5625bac68
commit
a6b68327b2
105
Lib/doctest.py
105
Lib/doctest.py
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
Loading…
Reference in New Issue