]> granicus.if.org Git - python/commitdiff
- Changed option directives to be example-specific. (i.e., they now
authorEdward Loper <edloper@gradient.cis.upenn.edu>
Thu, 12 Aug 2004 02:27:44 +0000 (02:27 +0000)
committerEdward Loper <edloper@gradient.cis.upenn.edu>
Thu, 12 Aug 2004 02:27:44 +0000 (02:27 +0000)
  modify option flags for a single example; they do not turn options
  on or off.)
- Added "indent" and "options" attributes for Example
- Got rid of add_newlines param to DocTestParser._parse_example (it's
  no longer needed; Example's constructor now takes care of it).
- Added some docstrings

Lib/doctest.py
Lib/test/test_doctest.py

index 31c0af8d6e00bd65ad7c3e2197bb0acd73a4a8b6..6927779ff7a49df16314cfb6da11c57f22cd94ff 100644 (file)
@@ -367,19 +367,29 @@ class Example:
     A single doctest example, consisting of source code and expected
     output.  `Example` defines the following attributes:
 
-      - source:  A single Python statement, always ending with a newline.
+      - source: A single Python statement, always ending with a newline.
         The constructor adds a newline if needed.
 
-      - want:  The expected output from running the source code (either
+      - want: The expected output from running the source code (either
         from stdout, or a traceback in case of exception).  `want` ends
         with a newline unless it's empty, in which case it's an empty
         string.  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
         zero-based, with respect to the beginning of the DocTest.
+
+      - indent: The example's indentation in the DocTest string.
+        I.e., the number of space characters that preceed the
+        example's first prompt.
+
+      - options: A dictionary mapping from option flags to True or
+        False, which is used to override default options for this
+        example.  Any option flags not contained in this dictionary
+        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):
+    def __init__(self, source, want, lineno, indent=0, options=None):
         # Normalize inputs.
         if not source.endswith('\n'):
             source += '\n'
@@ -389,6 +399,9 @@ class Example:
         self.source = source
         self.want = want
         self.lineno = lineno
+        self.indent = indent
+        if options is None: options = {}
+        self.options = options
 
 class DocTest:
     """
@@ -515,13 +528,16 @@ class DocTestParser:
         for m in self._EXAMPLE_RE.finditer(string.expandtabs()):
             # 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)
+            # Extract extra options from the source.
+            options = self._find_options(source, name, lineno)
+            # If it contains no real source, then ignore it.
             if self._IS_BLANK_OR_COMMENT(source):
                 continue
-            examples.append( Example(source, want, lineno) )
-
+            # Create an Example, and add it to the list.
+            examples.append( Example(source, want, lineno,
+                                     len(m.group('indent')), options) )
             # Update lineno (lines inside this example)
             lineno += string.count('\n', m.start(), m.end())
             # Update charno.
@@ -578,7 +594,7 @@ class DocTestParser:
                 lineno += len(lines)
 
             # Extract source/want from the regexp match.
-            (source, want) = self._parse_example(m, name, lineno, False)
+            (source, want) = self._parse_example(m, name, lineno)
             # Display the source
             output.append(source)
             # Display the expected output, if any
@@ -600,7 +616,17 @@ class DocTestParser:
         # Combine the output, and return it.
         return '\n'.join(output)
 
-    def _parse_example(self, m, name, lineno, add_newlines=True):
+    def _parse_example(self, m, name, lineno):
+        """
+        Given a regular expression match from `_EXAMPLE_RE` (`m`),
+        return a pair `(source, want)`, where `source` is the matched
+        example's source code (with prompts and indentation stripped);
+        and `want` is the example's expected output (with indentation
+        stripped).
+
+        `name` is the string's name, and `lineno` is the line number
+        where the example starts; both are used for error messages.
+        """
         # Get the example's indentation level.
         indent = len(m.group('indent'))
 
@@ -610,8 +636,6 @@ class DocTestParser:
         self._check_prompt_blank(source_lines, indent, name, lineno)
         self._check_prefix(source_lines[1:], ' '*indent+'.', name, lineno)
         source = '\n'.join([sl[indent+4:] for sl in source_lines])
-        if len(source_lines) > 1 and add_newlines:
-            source += '\n'
 
         # Divide want into lines; check that it's properly
         # indented; and then strip the indentation.
@@ -619,12 +643,47 @@ class DocTestParser:
         self._check_prefix(want_lines, ' '*indent, name,
                            lineno+len(source_lines))
         want = '\n'.join([wl[indent:] for wl in want_lines])
-        if len(want) > 0 and add_newlines:
-            want += '\n'
 
         return source, want
 
+    # This regular expression looks for option directives in the
+    # source code of an example.  Option directives are comments
+    # starting with "doctest:".  Warning: this may give false
+    # positives for string-literals that contain the string
+    # "#doctest:".  Eliminating these false positives would require
+    # actually parsing the string; but we limit them by ignoring any
+    # line containing "#doctest:" that is *followed* by a quote mark.
+    _OPTION_DIRECTIVE_RE = re.compile(r'#\s*doctest:\s*([^\n\'"]*)$',
+                                      re.MULTILINE)
+
+    def _find_options(self, source, name, lineno):
+        """
+        Return a dictionary containing option overrides extracted from
+        option directives in the given source string.
+
+        `name` is the string's name, and `lineno` is the line number
+        where the example starts; both are used for error messages.
+        """
+        options = {}
+        # (note: with the current regexp, this will match at most once:)
+        for m in self._OPTION_DIRECTIVE_RE.finditer(source):
+            option_strings = m.group(1).replace(',', ' ').split()
+            for option in option_strings:
+                if (option[0] not in '+-' or
+                    option[1:] not in OPTIONFLAGS_BY_NAME):
+                    raise ValueError('line %r of the doctest for %s '
+                                     'has an invalid option: %r' %
+                                     (lineno+1, name, option))
+                flag = OPTIONFLAGS_BY_NAME[option[1:]]
+                options[flag] = (option[0] == '+')
+        if options and self._IS_BLANK_OR_COMMENT(source):
+            raise ValueError('line %r of the doctest for %s has an option '
+                             'directive on a line with no example: %r' %
+                             (lineno, name, source))
+        return options
+
     def _comment_line(self, line):
+        "Return a commented form of the given line"
         line = line.rstrip()
         if line:
             return '#  '+line
@@ -632,6 +691,12 @@ class DocTestParser:
             return '#'
 
     def _check_prompt_blank(self, lines, indent, name, lineno):
+        """
+        Given the lines of a source string (including prompts and
+        leading indentation), check to make sure that every prompt is
+        followed by a space character.  If any line is not followed by
+        a space character, then raise ValueError.
+        """
         for i, line in enumerate(lines):
             if len(line) >= indent+4 and line[indent+3] != ' ':
                 raise ValueError('line %r of the docstring for %s '
@@ -640,6 +705,10 @@ class DocTestParser:
                                   line[indent:indent+3], line))
 
     def _check_prefix(self, lines, prefix, name, lineno):
+        """
+        Check that every line in the given list starts with the given
+        prefix; if any line does not, then raise a ValueError.
+        """
         for i, line in enumerate(lines):
             if line and not line.startswith(prefix):
                 raise ValueError('line %r of the docstring for %s has '
@@ -1105,32 +1174,6 @@ class DocTestRunner:
                                ('most recent call last', 'innermost last'),
                                re.MULTILINE | re.DOTALL)
 
-    _OPTION_DIRECTIVE_RE = re.compile('\s*doctest:\s*(?P<flags>[^#\n]*)')
-
-    def __handle_directive(self, example):
-        """
-        Check if the given example is actually a directive to doctest
-        (to turn an optionflag on or off); and if it is, then handle
-        the directive.
-
-        Return true iff the example is actually a directive (and so
-        should not be executed).
-
-        """
-        m = self._OPTION_DIRECTIVE_RE.match(example.source)
-        if m is None:
-            return False
-
-        for flag in m.group('flags').upper().split():
-            if (flag[:1] not in '+-' or
-                flag[1:] not in OPTIONFLAGS_BY_NAME):
-                raise ValueError('Bad doctest option directive: '+flag)
-            if flag[0] == '+':
-                self.optionflags |= OPTIONFLAGS_BY_NAME[flag[1:]]
-            else:
-                self.optionflags &= ~OPTIONFLAGS_BY_NAME[flag[1:]]
-        return True
-
     def __run(self, test, compileflags, out):
         """
         Run the examples in `test`.  Write the outcome of each example
@@ -1150,10 +1193,14 @@ class DocTestRunner:
 
         # Process each example.
         for example in test.examples:
-            # Check if it's an option directive.  If it is, then handle
-            # it, and go on to the next example.
-            if self.__handle_directive(example):
-                continue
+            # Merge in the example's options.
+            self.optionflags = original_optionflags
+            if example.options:
+                for (optionflag, val) in example.options.items():
+                    if val:
+                        self.optionflags |= optionflag
+                    else:
+                        self.optionflags &= ~optionflag
 
             # Record that we started this example.
             tries += 1
@@ -1349,12 +1396,13 @@ class OutputChecker:
     """
     def check_output(self, want, got, optionflags):
         """
-        Return True iff the actual output (`got`) matches the expected
-        output (`want`).  These strings are always considered to match
-        if they are identical; but depending on what option flags the
-        test runner is using, several non-exact match types are also
-        possible.  See the documentation for `TestRunner` for more
-        information about option flags.
+        Return True iff the actual output from an example (`got`)
+        matches the expected output (`want`).  These strings are
+        always considered to match if they are identical; but
+        depending on what option flags the test runner is using,
+        several non-exact match types are also possible.  See the
+        documentation for `TestRunner` for more information about
+        option flags.
         """
         # Handle the common case first, for efficiency:
         # if they're string-identical, always return true.
@@ -1411,7 +1459,10 @@ class OutputChecker:
     def output_difference(self, want, got, optionflags):
         """
         Return a string describing the differences between the
-        expected output (`want`) and the actual output (`got`).
+        expected output for an example (`want`) and the actual output
+        (`got`).  `optionflags` is the set of option flags used to
+        compare `want` and `got`.  `indent` is the indentation of the
+        original example.
         """
         # If <BLANKLINE>s are being used, then replace <BLANKLINE>
         # with blank lines in the expected output string.
index 7e583c5f330eea03f2934e5a0a4628a0b610b84c..977ade7eaedddfac3884a2593724e3e2e4331f2f 100644 (file)
@@ -267,20 +267,16 @@ Finding Tests in Functions
 For a function whose docstring contains examples, DocTestFinder.find()
 will return a single test (for that function's docstring):
 
-    >>> # Allow ellipsis in the following examples (since the filename
-    >>> # and line number in the traceback can vary):
-    >>> doctest: +ELLIPSIS
-
     >>> finder = doctest.DocTestFinder()
     >>> tests = finder.find(sample_func)
-    >>> print tests
+    
+    >>> print tests  # doctest: +ELLIPSIS
     [<DocTest sample_func from ...:12 (1 example)>]
+    
     >>> e = tests[0].examples[0]
     >>> (e.source, e.want, e.lineno)
     ('print sample_func(22)\n', '44\n', 3)
 
-    >>> doctest: -ELLIPSIS # Turn ellipsis back off
-
 If an object has no docstring, then a test is not created for it:
 
     >>> def no_docstring(v):
@@ -638,10 +634,6 @@ message is raised, then it is reported as a failure:
 If an exception is raised but not expected, then it is reported as an
 unexpected exception:
 
-    >>> # Allow ellipsis in the following examples (since the filename
-    >>> # and line number in the traceback can vary):
-    >>> doctest: +ELLIPSIS
-
     >>> def f(x):
     ...     r'''
     ...     >>> 1/0
@@ -649,6 +641,7 @@ unexpected exception:
     ...     '''
     >>> test = doctest.DocTestFinder().find(f)[0]
     >>> doctest.DocTestRunner(verbose=False).run(test)
+    ... # doctest: +ELLIPSIS
     **********************************************************************
     Failure in example: 1/0
     from line #1 of f
@@ -657,8 +650,6 @@ unexpected exception:
           ...
         ZeroDivisionError: integer division or modulo by zero
     (1, 1)
-
-    >>> doctest: -ELLIPSIS # Turn ellipsis back off:
 """
     def optionflags(): r"""
 Tests of `DocTestRunner`'s option flag handling.
@@ -863,20 +854,57 @@ and actual outputs to be displayed using a context diff:
     def option_directives(): r"""
 Tests of `DocTestRunner`'s option directive mechanism.
 
-Option directives can be used to turn option flags on or off from
-within a DocTest case.  The following example shows how a flag can be
-turned on and off.  Note that comments on the same line as the option
-directive are ignored.
+Option directives can be used to turn option flags on or off for a
+single example.  To turn an option on for an example, follow that
+example with a comment of the form ``# doctest: +OPTION``:
+
+    >>> def f(x): r'''
+    ...     >>> print range(10)       # should fail: no ellipsis
+    ...     [0, 1, ..., 9]
+    ...
+    ...     >>> print range(10)       # doctest: +ELLIPSIS
+    ...     [0, 1, ..., 9]
+    ...     '''
+    >>> test = doctest.DocTestFinder().find(f)[0]
+    >>> doctest.DocTestRunner(verbose=False).run(test)
+    **********************************************************************
+    Failure in example: print range(10)       # should fail: no ellipsis
+    from line #1 of f
+    Expected: [0, 1, ..., 9]
+    Got: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+    (1, 2)
+
+To turn an option off for an example, follow that example with a
+comment of the form ``# doctest: -OPTION``:
+
+    >>> def f(x): r'''
+    ...     >>> print range(10)
+    ...     [0, 1, ..., 9]
+    ...
+    ...     >>> # should fail: no ellipsis
+    ...     >>> print range(10)       # doctest: -ELLIPSIS
+    ...     [0, 1, ..., 9]
+    ...     '''
+    >>> test = doctest.DocTestFinder().find(f)[0]
+    >>> doctest.DocTestRunner(verbose=False,
+    ...                       optionflags=doctest.ELLIPSIS).run(test)
+    **********************************************************************
+    Failure in example: print range(10)       # doctest: -ELLIPSIS
+    from line #6 of f
+    Expected: [0, 1, ..., 9]
+    Got: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+    (1, 2)
 
+Option directives affect only the example that they appear with; they
+do not change the options for surrounding examples:
+    
     >>> def f(x): r'''
     ...     >>> print range(10)       # Should fail: no ellipsis
     ...     [0, 1, ..., 9]
     ...
-    ...     >>> doctest: +ELLIPSIS    # turn ellipsis on.
-    ...     >>> print range(10)       # Should succeed
+    ...     >>> print range(10)       # doctest: +ELLIPSIS
     ...     [0, 1, ..., 9]
     ...
-    ...     >>> doctest: -ELLIPSIS    # turn ellipsis back off.
     ...     >>> print range(10)       # Should fail: no ellipsis
     ...     [0, 1, ..., 9]
     ...     '''
@@ -889,18 +917,19 @@ directive are ignored.
     Got: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
     **********************************************************************
     Failure in example: print range(10)       # Should fail: no ellipsis
-    from line #9 of f
+    from line #7 of f
     Expected: [0, 1, ..., 9]
     Got: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
     (2, 3)
 
-Multiple flags can be toggled by a single option directive:
+Multiple options may be modified by a single option directive.  They
+may be separated by whitespace, commas, or both:
 
     >>> def f(x): r'''
     ...     >>> print range(10)       # Should fail
     ...     [0, 1,  ...,   9]
-    ...     >>> doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
     ...     >>> print range(10)       # Should succeed
+    ...     ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
     ...     [0, 1,  ...,   9]
     ...     '''
     >>> test = doctest.DocTestFinder().find(f)[0]
@@ -911,6 +940,104 @@ Multiple flags can be toggled by a single option directive:
     Expected: [0, 1,  ...,   9]
     Got: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
     (1, 2)
+
+    >>> def f(x): r'''
+    ...     >>> print range(10)       # Should fail
+    ...     [0, 1,  ...,   9]
+    ...     >>> print range(10)       # Should succeed
+    ...     ... # doctest: +ELLIPSIS,+NORMALIZE_WHITESPACE
+    ...     [0, 1,  ...,   9]
+    ...     '''
+    >>> test = doctest.DocTestFinder().find(f)[0]
+    >>> doctest.DocTestRunner(verbose=False).run(test)
+    **********************************************************************
+    Failure in example: print range(10)       # Should fail
+    from line #1 of f
+    Expected: [0, 1,  ...,   9]
+    Got: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+    (1, 2)
+
+    >>> def f(x): r'''
+    ...     >>> print range(10)       # Should fail
+    ...     [0, 1,  ...,   9]
+    ...     >>> print range(10)       # Should succeed
+    ...     ... # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+    ...     [0, 1,  ...,   9]
+    ...     '''
+    >>> test = doctest.DocTestFinder().find(f)[0]
+    >>> doctest.DocTestRunner(verbose=False).run(test)
+    **********************************************************************
+    Failure in example: print range(10)       # Should fail
+    from line #1 of f
+    Expected: [0, 1,  ...,   9]
+    Got: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+    (1, 2)
+
+The option directive may be put on the line following the source, as
+long as a continuation prompt is used:
+
+    >>> def f(x): r'''
+    ...     >>> print range(10)
+    ...     ... # doctest: +ELLIPSIS
+    ...     [0, 1, ..., 9]
+    ...     '''
+    >>> test = doctest.DocTestFinder().find(f)[0]
+    >>> doctest.DocTestRunner(verbose=False).run(test)
+    (0, 1)
+    
+For examples with multi-line source, the option directive may appear
+at the end of any line:
+
+    >>> def f(x): r'''
+    ...     >>> for x in range(10): # doctest: +ELLIPSIS
+    ...     ...     print x,
+    ...     0 1 2 ... 9
+    ...
+    ...     >>> for x in range(10):
+    ...     ...     print x,        # doctest: +ELLIPSIS
+    ...     0 1 2 ... 9
+    ...     '''
+    >>> test = doctest.DocTestFinder().find(f)[0]
+    >>> doctest.DocTestRunner(verbose=False).run(test)
+    (0, 2)
+
+If more than one line of an example with multi-line source has an
+option directive, then they are combined:
+
+    >>> def f(x): r'''
+    ...     Should fail (option directive not on the last line):
+    ...         >>> for x in range(10): # doctest: +ELLIPSIS
+    ...         ...     print x,        # doctest: +NORMALIZE_WHITESPACE
+    ...         0  1    2...9
+    ...     '''
+    >>> test = doctest.DocTestFinder().find(f)[0]
+    >>> doctest.DocTestRunner(verbose=False).run(test)
+    (0, 1)
+
+It is an error to have a comment of the form ``# doctest:`` that is
+*not* followed by words of the form ``+OPTION`` or ``-OPTION``, where
+``OPTION`` is an option that has been registered with
+`register_option`:
+
+    >>> # Error: Option not registered
+    >>> s = '>>> print 12   #doctest: +BADOPTION'
+    >>> test = doctest.DocTestParser().get_doctest(s, {}, 's', 's.py', 0)
+    Traceback (most recent call last):
+    ValueError: line 1 of the doctest for s has an invalid option: '+BADOPTION'
+
+    >>> # Error: No + or - prefix
+    >>> s = '>>> print 12   #doctest: ELLIPSIS'
+    >>> test = doctest.DocTestParser().get_doctest(s, {}, 's', 's.py', 0)
+    Traceback (most recent call last):
+    ValueError: line 1 of the doctest for s has an invalid option: 'ELLIPSIS'
+
+It is an error to use an option directive on a line that contains no
+source:
+
+    >>> s = '>>> # doctest: +ELLIPSIS'
+    >>> test = doctest.DocTestParser().get_doctest(s, {}, 's', 's.py', 0)
+    Traceback (most recent call last):
+    ValueError: line 0 of the doctest for s has an option directive on a line with no example: '# doctest: +ELLIPSIS'
 """
 
 def test_testsource(): r"""
@@ -971,12 +1098,12 @@ Create some fake stdin input, to feed to the debugger:
 
 Run the debugger on the docstring, and then restore sys.stdin.
 
-    >>> doctest: +NORMALIZE_WHITESPACE
     >>> try:
     ...     doctest.debug_src(s)
     ... finally:
     ...      sys.stdin = real_stdin
     ...      fake_stdin.close()
+    ... # doctest: +NORMALIZE_WHITESPACE
     > <string>(1)?()
     (Pdb) 12
     --Return--
@@ -1019,8 +1146,7 @@ def test_pdb_set_trace():
       >>> real_stdin = sys.stdin
       >>> sys.stdin = fake_stdin
 
-      >>> doctest: +ELLIPSIS
-      >>> runner.run(test)
+      >>> runner.run(test) # doctest: +ELLIPSIS
       --Return--
       > ...set_trace()->None
       -> Pdb().set_trace()
@@ -1057,7 +1183,7 @@ def test_pdb_set_trace():
       >>> real_stdin = sys.stdin
       >>> sys.stdin = fake_stdin
 
-      >>> runner.run(test)
+      >>> runner.run(test) # doctest: +ELLIPSIS
       --Return--
       > ...set_trace()->None
       -> Pdb().set_trace()
@@ -1068,8 +1194,6 @@ def test_pdb_set_trace():
       (Pdb) > <string>(1)?()
       (Pdb) 1
       (Pdb) (0, 2)
-
-      >>> doctest: -ELLIPSIS
       """
 
 def test_DocTestSuite():