summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/doctest.py105
-rw-r--r--Lib/test/test_doctest.py93
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<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
# 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<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):
"""
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"""