]> granicus.if.org Git - python/commitdiff
Added an "exc_msg" attribute to Example (containing the expected
authorEdward Loper <edloper@gradient.cis.upenn.edu>
Thu, 26 Aug 2004 00:05:43 +0000 (00:05 +0000)
committerEdward Loper <edloper@gradient.cis.upenn.edu>
Thu, 26 Aug 2004 00:05:43 +0000 (00:05 +0000)
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
Lib/test/test_doctest.py

index 01f7cb32367cece45b4bb60e1202dd74f73ecdea..2cd96ba56bcd2d1fbc843c9c4fecf682706e8d73 100644 (file)
@@ -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
index 5d0cf90be5a07ee91bbb9888de51a84b66153fae..a9076a6f88c6af504f05ca62d2a7b986ccb19bfd 100644 (file)
@@ -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"""