From 74bca7aa44be315c669edcc0e02126fdd927062b Mon Sep 17 00:00:00 2001 From: Edward Loper Date: Thu, 12 Aug 2004 02:27:44 +0000 Subject: - Changed option directives to be example-specific. (i.e., they now 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 | 151 ++++++++++++++++++++++++++------------- Lib/test/test_doctest.py | 182 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 254 insertions(+), 79 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 31c0af8..6927779 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -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[^#\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 s are being used, then replace # with blank lines in the expected output string. diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py index 7e583c5..977ade7 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -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 [] + >>> 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 > (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) > (1)?() (Pdb) 1 (Pdb) (0, 2) - - >>> doctest: -ELLIPSIS """ def test_DocTestSuite(): -- cgit v0.12